Compare commits

..
Author SHA1 Message Date
Brent Vatne 6489ddc6ad v12.5.2 2024-10-11 11:46:46 -07:00
Brent VatneandKeith Kurak 496e68935a [eas-cli] Add message about concurrency add-on (#2611)
Co-authored-by: Keith Kurak <[email protected]>
2024-10-11 10:23:40 -07:00
Will Schurman a346af2869 [eas-cli] Unhide rollout-percentage flag on update publish command (#2608) 2024-10-07 11:03:32 -07:00
Szymon Dziedzic 7699573100 [eas-json] update eas.schema.json (#2606)
# Why

add new image to eas.schema.json

# How

add new image to eas.schema.json
2024-10-03 17:41:16 +02:00
Piotr Szeremeta 3f832765c2 Refactor env:unlink, add tests (#2591)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Update command to use multiple environments, polish the interface.

# How

* use `variable-name` and `variable-environment` to select the variable
* Use `multiselect` and multiple `environment` parameters to allow unlinking multiple envs

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

# Test Plan

Added tests
2024-10-01 11:03:30 +02:00
Piotr Szeremeta af16cbaaea Refactor env:list and add tests (#2588)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Allow selecting multiple environments to display variables from.

# How

* Use `multiselect`, allow specifying multiple `environment` parameters
* List variables from all environments

# Test Plan

Added tests
2024-10-01 11:00:32 +02:00
Piotr Szeremeta 0d384d1722 Refactor env:link, add tests (#2587)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Add tests, polish the interface

# How

* Use `variable-name` and `variable-environment` to select the variable
* More robust selector of variable to remove ambiguity 
* Use multiselect to clearly indicate which environments are linked and which are not
* Automatic `link` and `unlink` action

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

# Test Plan

Added tests
2024-10-01 10:57:27 +02:00
Piotr Szeremeta 58d2088bf7 Refactor env:update, add tests (#2586)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Rework the EnvironmentVariableUpdate function - user is able to alter name, and environments.

# How

* Added `--variable-name` and `--variable-environment` parameters to identify the variable to update
* Using parameters from `--name` and `--environment` to update the variable using the new `updateEnvironmentVariable` method.

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

# Test Plan

Added tests
2024-10-01 10:54:28 +02:00
Piotr Szeremeta c622014b64 Add tests for EnvironmentVariableDelete (#2585)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Improve the selector for the variable to delete, rename parameter `name` to `variable-name`. 

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/KfcKByW8uA9rvjzBy0RA/e96c9c31-be76-401a-be0e-715a77b399d1.png)

# Test Plan

Added tests
2024-10-01 10:51:28 +02:00
Piotr Szeremeta 81feffa0c5 Add tests to EnvironmentVariableGet (#2584)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Polished the interface:

 - user is prompted for environment only when the name is ambiguous.
- changed `name` parameter to `variable-name`. 

All commands that were using `name` parameter to identify a single variable will use `variable-name` and `variable-environment` instead. This change will enable user to distinguish between `name` and `environment` that is identifying variable that is acted upon, and `name` and `environment` that is a parameter for command.

For example, when the user wants to update variable `TEST` that has environment `production` and change its name and change its environment, they can use:

`eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview`

# Test Plan

Added tests
2024-10-01 10:48:28 +02:00
Piotr Szeremeta 9521cc45da Update EnvironmentVariableCreate for multiple environments, add tests (#2583)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Update `eas env:create` to accept multiple `--environment` parameters.

# How

* Use `EASMultiEnvironmentFlag` in `EnvironmentVariableCreate`
* Update variable display with `environment` and `updatedAt` fields

# Test Plan

Added tests
2024-10-01 10:45:31 +02:00
Piotr Szeremeta 1dfe24d48a Allow prompting for multiple environments (#2582)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

EnvVar will support having multiple environments, CLI should support this.

# How

* Add an optional parameter `multiple` to `promptVariableEnvironmentAsync`
* Update commands using `promptVariableEnvironmentAsync`
* Add a new flag `EASMultiEnvironmentFlag`

# Test Plan

Tested manually, tests will be added in the following PRs
2024-10-01 10:42:27 +02:00
Piotr Szeremeta 99765c1802 Update EnvironmentVariableMutation and associated commands (#2581)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525 Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Bring up to date `create` and `update` EnvVar mutations.

# How

* Use `updateEnvironmentVariable` operation instead of `createEnvironmentVariable` with an 'overwrite' option.
* Add `type` parameter
* Add `environment` parameter for shared variables

# Test Plan

Tested manually, tests will be added in the following PRs
2024-10-01 10:39:28 +02:00
Piotr Szeremeta de388cfd27 Update Environment Variables queries: (#2580)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525 Update EAS-CLI for new EnvVar features
](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features) 

Update EnvVar queries - allow quering using environments.

# How

* Added optional `environment` parameter to queries
* Fix typing of the `--environment` parameter

# Test Plan

Tested manually
2024-10-01 10:36:06 +02:00
Will Schurman 37a68c5237 [ci] Migrate from CODEOWNERS to codemention (#2603) 2024-09-27 09:06:46 -07:00
Will Schurman 6823e4f66c [eas-cli] Disallow republishing an update that is being rolled-out (#2602) 2024-09-27 09:05:24 -07:00
Phil Pluckthun ffa2f034f9 refactor(eas-cli): Implement updated deployment upload flow (#2601)
* Update API functions for new upload endpoint and deploy result

* Fix listWorkerFilesAsync file type

* Add call to finalize deployment

* Update CHANGELOG

* Add retries to /deploy/finalize call

* Apply lints
2024-09-27 10:36:24 +01:00
Expo CI 8243f5f0cb update CHANGELOG.md 2024-09-27 05:22:24 +00:00
Szymon Dziedzic 01676a4f88 v12.5.1 2024-09-26 22:16:28 -07:00
Szymon Dziedzic 7edc10feaf [eas-cli] add exit option to eas credentials interactive menu (#2570)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C017N0N99RA/p1726226230206999

Right now the only way to exit the `eas credentials` command is to kill the process by doing ctrl+c. Let's add an `exit` option to the interactive menu.

# How

Add exit option to menu.

# Test Plan

Test manually
2024-09-27 06:21:08 +02:00
Will Schurman fba26e6b50 [eas-cli] Add destination branch arguments to update:republish command (#2575) 2024-09-26 09:32:55 -07:00
Szymon Dziedzic 69014fe21d [eas-cli] stop using environment flag in build related commands (#2547)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

On Friday's meeting, we agreed that having both a flag and a field in the build profile for setting the environment might be too much. Additionally, we already have a `--profile` flag that has a `-e` alias which makes it even more confusing. We decided to delete the flag to avoid confusion and make it as straightforward as possible.

# How

Delete the `--environment` flag for build-related commands, rely only on the `environment` field in eas.json.

# Test Plan

Tests
2024-09-26 16:45:03 +02:00
Szymon Dziedzic 217a1f4fdb [eas-cli] add eas env:exec command (#2549)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Add the `eas env:exec` command. This command allows you to run any arbitrary bash command with env vars from a given environment.

# How

Add `eas env:exec` command

# Test Plan

Test manually

![Screenshot 2024-09-12 at 11.23.06.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/9YRXgAETSTRMfjZ0IP35/b1f2e6d3-7a4d-415d-a6e3-5754684c6349.png)
2024-09-26 16:44:16 +02:00
Cedric van Putten 0863b9a477 refactor(eas-cli): support assigning dev domain in non interactive mode (#2595)
* refactor(eas-cli): split prompt from dev domain assignment and support it non-interactive

* docs: add changelog entry

* docs: fix changelog entry

* fix(eas-cli): use proper name in "already taken" prompt

* chore: resolve linting issue
2024-09-26 12:34:14 +02:00
Stanisław Chmiela 081a254d9b [eas-cli] Replace logsUrl with logFiles (#2598)
# Why

Almost a year ago we've migrated from using a single file for submission logs to multi-file logs, accommodating need to display logs from both the worker and the scheduler that is starting the worker VM.

Companion to https://github.com/expo/universe/pull/16873.

# How

Replaced `logsUrl` with `logFiles`, adjusted code.

# Test Plan

Logs printed

<img width="1055" alt="Zrzut ekranu 2024-09-24 o 23 06 22" src="https://github.com/user-attachments/assets/6c9a45c0-6760-4629-82e3-a79d5ef99ebe">
2024-09-25 19:13:29 +02:00
Will Schurman ba7eb7ea07 [ci] Remove blurb about changelog bot since it is still disabled (#2592) 2024-09-24 13:19:46 -07:00
Cedric van Putten f5df6e6e77 refactor(eas-cli): allow Expo Router server deployments without client directory (#2597)
* refactor(eas-cli): allow Expo Router server deployments without client directories

* docs: fix changelog entry
2024-09-24 20:03:19 +02:00
Cedric van Putten c0c27559eb refactor(eas-cli): simplify the output of eas deploy --json (#2596)
* refactor(eas-cli): simplify the `eas deploy --json` output

* docs: fix changelog entry

* refactor(eas-cli): keep aliases as array in `--json` output

* docs: fix changelog entry
2024-09-24 16:45:07 +02:00
Phil Pluckthun 2e002ad709 Fix worker asset upload error not being shown (#2579) 2024-09-24 11:17:12 +01:00
Expo CI 2e211f4904 update CHANGELOG.md 2024-09-23 15:32:31 +00:00
Szymon Dziedzic 981f7c9727 v12.5.0 2024-09-23 17:25:42 +02:00
Stanisław Chmiela 24245d76cf [eas-cli] Add submit:internal (#2571)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

I want to add support for submissions as part of workflows, where, when we start the job, we don't have access to the repository. So, we need to resolve submission config _somewhere_, ideally where we are isolated, can easily clone project, install dependencies and resolve dynamic config.

As we do with `eas build:internal`, I expect it will make sense to do `eas submit:internal` which will do what `eas submit` does usually — resolve all the credentials, using local filesystem or Expo servers  and then fetch the _actual_ keys from Expo servers.

# How

First, I changed structure of `SubmitCommand`/`Submitter` a bit — previously, `SubmitCommand` would create the `Submitter` and call `submitAsync` _inside_ `runAsync()`. After my changes `SubmitCommand` will create _and return_ the `Submitter` and the call site will need to call `submitAsync`.

By doing it this way I was able to add a `getSubmissionInputAsync` to `BaseSubmitter` which returns all-resolved submission input, as-it-would-have-been-used-in-submission-mutation. This ensures the same behavior for `eas submit` and `eas submit:internal`.

Once we have that, `eas submit:internal` is relatively straightforward:
1. create `SubmitCommand`, get `Submitter`.
2. `getSubmissionInputAsync`.
3. fetch necessary credentials by IDs from `www` (https://github.com/expo/universe/pull/16793).
4. validate that we have it all (https://github.com/expo/eas-build/pull/443) and print the JSON.

# Test Plan

I was able to run `easd submit:internal` locally and got the JSON output.
2024-09-20 22:25:33 +02:00
Cedric van Putten d42cbe0146 fix(eas-cli): avoid malforming app.json with empty .expo object (#2573)
* fix(eas-cli): avoid malforming `app.json` with empty `.expo` object

* docs(cli): add changelog entry

* docs(cli): fix changelog entry

* test(eas-cli): drop the legacy test
2024-09-19 21:19:23 +02:00
Stanisław Chmiela 2cf6dd49db [eas-cli] Let iOS bundle identifier override always (#2576)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

In a submit job we're going to have an IPA/APK to submit. We don't want to resolve any bundle identifiers from the project, we can use one from that IPA/APK.

# How

Let the override env variable override bundle identifier resolution if the workflow is managed too.

# Test Plan

I used it to submit an IPA with workflows.
2024-09-19 16:17:01 +02:00
Expo Bot 09c5038c58 Upgrade eas-build dependencies (#2577)
⬆️ Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.
2024-09-19 15:50:53 +02:00
Cedric van Putten d33ebe1ae4 refactor(eas-cli): reword dev domain prompt to use "preview URL" (#2572)
* refactor(eas-cli): reword dev domain prompt to use "preview URL"

* docs(eas-cli): add changelog entry

* docs(eas-cli): fix changelog entry
2024-09-17 16:03:31 +02:00
Cedric van Putten 41fd76c154 fix(eas-cli): check if export is available before validating project (#2569)
* fix(eas-cli): check if export is available before validating project

* docs(eas-cli): add changelog entry

* docs(eas-cli): fix changelog entry
2024-09-16 12:20:01 +02:00
Radosław Krzemień dadf58fa70 [eas-cli] [ENG-10146] Allow submission of builds in progress (#2543)
* [eas-cli] Add tests

Added tests for allowing to select in progress builds

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Allow in progress builds

When prompting the user with a list of builds the in-progress builds are now returned first

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Adjust description

When prompting the user with a list of builds the build status is now displayed let the user know which are finished and which are still in progress

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Reformat

Adjusted formatting

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Reformat

Adjusted formatting

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Lint fix

lint --fix

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Add changelog

Added the changelog entry since the bot does not seem to be working

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Return more statuses and sort

Query returning the potential builds for submission now also returns build in `new` and `in-queue` statuses. All builds are then sorted decreasingly by `createdAt` and up to `limit` are returned

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Lint

yarn lint --fix

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to

* [eas-cli] Add mapping

Created build status mapping instead string modification

See: https://linear.app/expo/issue/ENG-10146/fr-add-option-to-trigger-auto-submit-while-waiting-for-build-to
2024-09-16 11:38:34 +02:00
Archimedes TrajanoandSzymon Dziedzic a7f939fb5f log command execution to assist in debugging local builds (#2526)
* log command execution to assist in debugging local builds

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* import and changelog

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

Co-authored-by: Szymon Dziedzic <[email protected]>

---------

Co-authored-by: Szymon Dziedzic <[email protected]>
2024-09-16 11:03:23 +02:00
Expo CI e54b5a243c update CHANGELOG.md 2024-09-14 14:15:52 +00:00
Cedric van Putten d2a89c1d77 v12.4.1 2024-09-14 15:10:44 +01:00
Cedric van Putten 9c4ff98d68 refactor(eas-cli): remove the cursor space when prompting dev domain after input (#2568)
* refactor(eas-cli): remove the cursor space when prompting dev domain after input

* docs(eas-cli): add changelog entry
2024-09-14 15:08:46 +01:00
Cedric van Putten d7dfef0c36 refactor(eas-cli): enhance the --environment help description (#2567)
* refactor(eas-cli): enhance the `--environment` help description

* docs(eas-cli): add changelog entry

* docs(eas-cli): add missing periods to `eas worker:alias --help`

* refactor(eas-cli): enhance the `--environment` help description
2024-09-14 15:03:40 +01:00
Cedric van Putten fdb30eeacc fix(eas-cli): restore "export not found" error and hide recent exports (#2566)
* fix(eas-cli): hide project export date when not older than 1m

* fix(eas-cli): add back no export error

* docs(eas-cli): add changelog entry

* chore(eas-cli): add `--platform=web` to error command

* docs(eas-cli): fix changelog entry
2024-09-14 15:03:20 +01:00
Expo CI d4c6440753 update CHANGELOG.md 2024-09-14 12:12:25 +00:00
Cedric van Putten 87b0cf41a8 v12.4.0 2024-09-14 13:07:15 +01:00
Cedric van Putten 7847b5297c refactor(eas-cli): always default to static uploads and add modified time (#2565)
* refactor(eas-cli): always default to static uploads and add modified time

* docs(eas-cli): add changelog entry

* chore(eas-cli): resolve linting issue

* fix(eas-cli): error when there are no deployments

* fix(eas-cli): remove unused `exp` var

* fix(eas-cli): fix json output again
2024-09-14 13:05:49 +01:00
Cedric van Putten 539ef8f3ad refactor(eas-cli): polish the dev domain prompt (#2564)
* refactor(eas-cli): polish the dev domain prompt

* docs(eas-cli): add changelog entry

* refactor(eas-cli): simplify the "already taken" message

* feature(eas-cli): add suggested project url to avoid conflicts
2024-09-14 12:44:55 +01:00
Cedric van Putten 819259ab1a feature(eas-cli): add worker:alias|promote --prod flag to promote existing deployments (#2563)
* feature(eas-cli): add `worker:alias --prod` flag to promote existing deployments

This also includes some clean ups and log utility sharing

* feature(eas-cli): support `worker:promote --prod --id`

* refactor(eas-cli): rename `/serverless/deployments` -> `/hosting/deployments`

* docs(eas-cli): add changelog entry

* chore(eas-cli): resolve linting issues

* fix(eas-cli): disable ora when possibly asking for a dev domain

* chore(eas-cli): resolve typing issue
2024-09-14 09:20:06 +01:00
Cedric van Putten 47e1989c9c feature(eas-cli): add worker:alias --json support for 3rd parties and custom tooling (#2562)
* feature(eas-cli): add `worker:alias --json` flag to integrate with 3rd parties

* docs(eas-cli): add changelog entry

* docs(eas-cli): fix changelog entry

* fix(eas-cli): lowercase deployment identifier

* fix(eas-cli): resolve linting issues
2024-09-13 16:14:21 +01:00
Cedric van Putten fa3262951b feature(eas-cli): add worker --json support for 3rd parties and custom tooling (#2561)
* feature(eas-cli): add `worker --json` flag to integrate with 3rd parties

* docs(eas-cli): add changelog entry

* refactor(eas-cli): rename `expoDashboardUrl` -> `dashboardUrl`

* chore(eas-cli): resolve linting issues

* docs(eas-cli): fix changelog entry
2024-09-13 16:09:04 +01:00
Cedric van Putten 66ce64e0a3 feature(eas-cli): add worker --export-dir to select exported directory (#2560)
* feature(eas-cli): add `worker --export-dir` flag to select export folder

* docs(eas-cli): add changelog entry

* fix(eas-cli): use `path.join` instead of `path.resolve`

`path.resolve` throws when the path does not exist, `path.join` uses our own validation errors.

* docs(eas-cli): fix changelog entry
2024-09-13 15:48:41 +01:00
Phil Pluckthun 5c553cee7b [eas-cli] Add --environment argument to worker:deploy (#2557)
* Add `--environment` argument to worker:deploy

* Add to changelog

* Apply lints
2024-09-13 13:15:30 +01:00
Cedric van Putten 2fb079d4bb refactor(eas-cli): update eas worker:alias with same polishes from eas worker (#2558)
* refactor(eas-cli): reuse same working in `eas worker:alis` from `eas worker`

* refactor(eas-cli): reuse same result table from `eas worker`

* docs(eas-cli): update changelog entry

* refactor(eas-cli): use `Alias URL` instead of `Aliased URL`
2024-09-13 13:06:47 +01:00
Cedric van Putten d15cd90c81 refactor(eas-cli): support eas worker --production and clean up command output (#2555)
* refactor(eas-cli): add support for both `--prod` and `--production` flags in `eas worker`

* chore: add changelog entry

* refactor(eas-cli): also tweak `--help` output

* refactor(eas-cli): move `--prod` before `--alias`/`--id` in usage

* refactor(eas-cli): clean up ora spinners and add table output

* refactor(eas-cli): simplify project type detection

* refactor(eas-cli): simplify alias and production spinners

* docs(eas-cli): update changelog entry

* chore: resolve linting issues

* refactor(eas-cli): use `Alias URL` instead of `Aliased URL`

* fix(eas-cli): mark `production` as bold in ora spinners
2024-09-13 12:53:11 +01:00
Cedric van Putten 0fbd3e47f8 fix(eas-cli): avoid adding expo.extra data from config plugins in eas init (#2554)
* fix(eas-cli): avoid adding `expo.extra` data from config plugins in `eas init`

Note that in the newer `@expo/config` package, modifying now also merges existing properties. Once thats released, we can use that instead of manually merging this data.

* docs(eas-cli): add changelog entry
2024-09-12 15:10:40 +02:00
Cedric van Putten 12efaf12cc feature(eas-cli): add worker --id flag (#2552)
* feature(eas-cli): add `worker --id` flag

* docs: add changelog entry
2024-09-12 13:02:47 +02:00
Cedric van PuttenandSzymon Dziedzic a404bb383d feature(eas-cli): add worker --alias flag (#2551)
* feature(eas-cli): add `worker --alias` flag

* docs: add changelog entry

* fix(eas-cli): add missing `ora.start`

Co-authored-by: Szymon Dziedzic <[email protected]>

---------

Co-authored-by: Szymon Dziedzic <[email protected]>
2024-09-12 12:43:01 +02:00
Cedric van PuttenandSzymon Dziedzic feaa8cbf22 feature(eas-cli): add worker --prod flag and hint for production deployments (#2550)
* feature(eas-cli): add `worker --prod` flag and hint for production deployments

* docs: add changelog entry

* refactor(eas-cli): add Syzmons PR feedback

Co-authored-by: Szymon Dziedzic <[email protected]>

---------

Co-authored-by: Szymon Dziedzic <[email protected]>
2024-09-12 12:37:00 +02:00
Cedric van Putten 542ea7a791 feature(eas-cli): add worker:alias command (#2548)
* feature(eas-cli): add `worker:alias` command to assign aliases

* feature(eas-cli): add worker selection when assigning alias

* refactor(eas-cli): clean up comments and prompt for alias

* refactor(eas-cli): clean up command and flag descriptions

* chore: resolve linting issues

* docs: add changelog entry
2024-09-12 12:25:29 +02:00
dependabot[bot] 7d80b0540f Bump express from 4.19.2 to 4.20.0 (#2546)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.20.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-12 09:14:25 +02:00
dependabot[bot] cc4e0ebda1 Bump dset from 3.1.2 to 3.1.4 (#2553)
Bumps [dset](https://github.com/lukeed/dset) from 3.1.2 to 3.1.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/lukeed/dset/releases">dset's releases</a>.</em></p>
<blockquote>
<h2>v3.1.3</h2>
<h2>Patches</h2>
<ul>
<li>Add <code>&quot;types&quot;</code> export conditions for TypeScript <code>&quot;nodenext&quot;</code>/<code>&quot;node16&quot;</code> resolution: <a href="https://redirect.github.com/lukeed/dset/issues/40">#40</a>
<em>Thank you <a href="https://github.com/Akkuma"><code>@​Akkuma</code></a></em></li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a href="https://github.com/lukeed/dset/compare/v3.1.2...v3.1.3">https://github.com/lukeed/dset/compare/v3.1.2...v3.1.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/lukeed/dset/commit/05b1ec0f8cebe3b5d5667285bc7da90f38a7215e"><code>05b1ec0</code></a> 3.1.4</li>
<li><a href="https://github.com/lukeed/dset/commit/16d6154e085bef01e99f01330e5a421a7f098afa"><code>16d6154</code></a> fix: prevent <strong>proto</strong> assignment via implicit string</li>
<li><a href="https://github.com/lukeed/dset/commit/48f14a1a4b2985c30663133e4699ddab69a393ac"><code>48f14a1</code></a> 3.1.3</li>
<li><a href="https://github.com/lukeed/dset/commit/aca90fb54b908de0042b7da0d45ef3ca0ea9671a"><code>aca90fb</code></a> chore: add &quot;types&quot; conditions for TS &quot;nodenext&quot; resolution (<a href="https://redirect.github.com/lukeed/dset/issues/40">#40</a>)</li>
<li>See full diff in <a href="https://github.com/lukeed/dset/compare/v3.1.2...v3.1.4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dset&package-manager=npm_and_yarn&previous-version=3.1.2&new-version=3.1.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/expo/eas-cli/network/alerts).

</details>
2024-09-12 09:13:49 +02:00
Phil Pluckthun 1621fb3a9b feat(cli): Source @expo/env dotenv vars for worker deployments (#2545)
* Add `@expo/env`

* Source env variables from ".env" via `@expo/env` to deployment

* Add CHANGELOG

* Increase requested range for `@expo/env`

* Apply lints
2024-09-11 13:03:08 +01:00
Phil Pluckthun 51b5946854 chore(cli): Add --non-interactive flag to fix worker:deploy initialisation (#2544) 2024-09-10 18:00:03 +01:00
Szymon Dziedzic b8ed63cadc [eas-cli] improve error message when CFBundleShortVersionString has invalid format (#2542)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C017N0N99RA/p1725915931423649

# How

Improve error message to make it more actionable

# Test Plan

Test manually

![Screenshot 2024-09-10 at 12.39.27.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/9YRXgAETSTRMfjZ0IP35/0c5ad653-3e55-43e0-bc12-aa4b30f730eb.png)

![Screenshot 2024-09-10 at 12.34.51.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/9YRXgAETSTRMfjZ0IP35/2e8f9e21-1d06-4c6c-8cc0-f263c9455bae.png)
2024-09-10 16:01:42 +02:00
Expo CI 8fc0648419 update CHANGELOG.md 2024-09-09 15:41:29 +00:00
Szymon Dziedzic 4fbc6aa3ff v12.3.0 2024-09-09 17:36:14 +02:00
Szymon Dziedzic 83bf6de727 [eas-cli] warn people if pulling secret env vars in env:pull command (#2538)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C06FK950085/p1725586296781109

# How

warn people if pulling secret env vars in env:pull command

# Test Plan

![Screenshot 2024-09-06 at 15.36.44.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/9YRXgAETSTRMfjZ0IP35/d1ecf95a-a9f4-4d01-b24e-5b2e9ea5e31c.png)
2024-09-09 17:30:58 +02:00
Szymon Dziedzic a2219beaba v12.2.0 2024-09-09 17:25:30 +02:00
90a2612792 Added --non-interactive and --force support when --id is not passed to eas init command (#1983)
* Added --non-interactive and --force support when --id is not passed to eas init command

* Removed id dependency from init command flags

* Bump undici from 5.19.1 to 5.26.3

Bumps [undici](https://github.com/nodejs/undici) from 5.19.1 to 5.26.3.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.19.1...v5.26.3)

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

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

* Revert "Bump undici from 5.19.1 to 5.26.3"

* Apply suggestions from code review

Co-authored-by: Szymon Dziedzic <[email protected]>

* Fixed initializeWithoutExplicitIDAsync not being used

* Require force flag to use default account when no account name is specified in non-interactive mode

* Changed error message when user has access to multiple accounts in non-interactive mode

* Added changelog entry

* Update CHANGELOG.md

Co-authored-by: Szymon Dziedzic <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ryan Button <[email protected]>
Co-authored-by: Szymon Dziedzic <[email protected]>
2024-09-09 17:23:53 +02:00
Expo CI d94b57c1b7 update CHANGELOG.md 2024-09-09 15:01:14 +00:00
Szymon Dziedzic d63fc1f01e v12.1.1 2024-09-09 16:54:42 +02:00
Szymon Dziedzic 57e7481a68 fix changelog 2024-09-09 16:54:21 +02:00
HarrisandHarris Han 7bb18e2489 [eas-cli] Fix entitlements resolution for multiple iOS targets (#2532)
* [fix]: correctly retrieve entitlements for extensions using their own target name

Fixed an issue where extensions were incorrectly retrieving the main app's entitlements instead of their own. Updated the code to use `dependency.name` for fetching the correct entitlements.

* update CHANGELOG.md

---------

Co-authored-by: Harris Han <[email protected]>
2024-09-09 16:50:08 +02:00
Expo CI 359464d55c update CHANGELOG.md 2024-09-06 13:13:04 +00:00
Phil Pluckthun e993e9a370 v12.1.0 2024-09-06 14:07:33 +01:00
75046b0b35 feat(eas-cli): Implement initial worker:deploy command (#2447)
* Update generated schema

* Add initial structure for worker code

* Add generated types to mutation

* Add initial command implementation

* Fix linting

* [ENG-12738] prompt for dev domain name (#2452)

* Add a dist folder check

* Prompt adding dev domain name on the CLI

* Validate for dev domain name taken

* Add progress indicator and 413 error message

* feat: Implement new upload process with new `deployment-api` endpoints (#2487)

* Add utilities for new worker upload process

* Add updated upload method

* Add server/client folder errors

* Implement new upload process

* Delete code for old upload process

* Fix fetch helper obfuscating errors

* Add server text to error

* Temporarily disable compression

* Apply lints

* refactor(worker): Implement batched uploads and improved progress output (#2490)

* Add refactored upload and batched upload utilities

* Switch to batch uploading utilities

* Add console output

* Apply lints

* Fix typo

* Fix error output

* Ignore system files while uploading (#2492)

* Mark worker:deploy as hidden command

* Replace ts-ignore directive comments

* Add changelog entry

* Apply suggestions from code review

Co-authored-by: Szymon Dziedzic <[email protected]>
Co-authored-by: Cedric van Putten <[email protected]>

* Switch to dynamic project config

* Remove superfluous formatSourcemap code

* Add async suffix

* Add max file-size note

* Normalise to throws

* Apply lints

* Log out eas url after deploy

* feat(eas-cli): Add support for static worker deployments (#2536)

* Add fallback logic for static projects

* Read project config for web.output mode

* Update GraphQL codegen'd files

---------

Co-authored-by: Kadi Kraman <[email protected]>
Co-authored-by: Szymon Dziedzic <[email protected]>
Co-authored-by: Cedric van Putten <[email protected]>
2024-09-06 13:39:44 +01:00
Szymon Dziedzic 3d55a4df40 [eas-cli] fix env vars loading issue and be more verbose about where the given env var comes from (#2534)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

We had an invalid type guard that caused our GQL requests to fetch env vars to fail because of the environment enum option being in lowercase vs uppercase.

# How

Fix the issue and type guard.

Be more verbose about the source of the env vars being loaded to make it easier to understand what's going on

# Test Plan

Before

![Screenshot 2024-09-04 at 12.40.15.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/9YRXgAETSTRMfjZ0IP35/32d06cbf-c605-4a34-a336-9a715792da9c.png)

After

![Screenshot 2024-09-04 at 13.51.01.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/9YRXgAETSTRMfjZ0IP35/9a1451c3-c6b7-4f2f-9f86-e1254e0580a8.png)

![Screenshot 2024-09-04 at 13.54.12.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/9YRXgAETSTRMfjZ0IP35/c5da456a-7c9e-4c42-b69d-592f3e76909a.png)
2024-09-06 10:48:36 +02:00
Will Schurman b32ff2e733 [eas-cli] Upload fingeprint source as part of eas update command (#2533) 2024-09-05 11:59:58 -07:00
Expo CI 6245fb418d update CHANGELOG.md 2024-09-04 08:56:25 +00:00
Szymon Dziedzic 50943f333a v12.0.0 2024-09-04 10:50:36 +02:00
Szymon Dziedzic e930383a10 upgrade eas-cli-local-build-plugin to 1.0.135 2024-09-04 10:50:28 +02:00
Radosław KrzemieńandSzymon Dziedzic 8c31825908 [eas-cli] [ENG-11843] Change default appVersionSource to REMOTE (#2411)
* [eas-cli] Update tests

Updated the tests to reflect the change of the default `appVersionSource` value

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update remote version handling

Updated the logic for REMOTE version source to treat undefined `appVersionSource` as REMOTE instead of LOCAL

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Refactor tests

Extracted the helper functions overriding the `appVersionSource` in eas.json to utils.ts

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update version source when running builds

Applied the same change to the build command as was applied to version commands

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* update CHANGELOG.md

* [eas-cli] Adjust changelog

After it was correctly pointed out by @sjchmiela, the changelog has been amended to list the current changes not as chores and as breaking change instead

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Prompt the user

The user is now prompted when they don't specify `appVersionSource` to either set it automatically to LOCAL/REMOTE or abort the command and configure it manually. This removes the default behaviour alltogether and avoids a situation where some users have been relying on the old default and would now be surprised

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update message

Updated the message displayed to the user when prompting for selection after the review

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update message

Updated the message displayed to the user when prompting for selection after the review

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update default eas.json

Updated the default eas.json created with `configure` command

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update default eas.json

Updated the default eas.json created with `onboarding` command

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Check for non-interactive

Made the function prompting the user for selection on what to do with appVersionSource to respect the non-interactive flag, and abort the operation in non-interactive mode since the user can't make a selection

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Remove code duplication

Moved the setting of appVersionSource in easJsonCliConfig into the function prompting the user, which allows for implementing that in one place instead of 3

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Fix circular dependency

There was a circular dependency introduced with passing of the flags into a function. The definition of the build flags has been moved into the types.ts file to avoid itĻ

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* [eas-cli] Adjust wording

Changed the wording of the message displayed to the user to more accurately describe how app versions are handled

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update default eas.json

Added autoIncrement: true when creating the default eas.json with remote appVersionSource

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Don't implicitly assume remote appVersionSource

The user is required to set appVersionSource in interactive mode so it should always be set. In non-interactive mode it can be not set, and in that case the default remains the same as it was, which is local

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Don't implicitly assume remote appVersionSource

The user is required to set appVersionSource in interactive mode so it should always be set. In non-interactive mode it can be not set, and in that case the default remains the same as it was, which is local

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Set autoIncrement

When creating the default eas.json set autoIncrement: true

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Set appVersionSource to local for non-interactive

To not break CIs that the users built for their apps in non-interactive mode we need to select the appVersionSource automatically in those cases. And since they may be counting on it being local since that was the previous default we need to keep setting it to local even if the new default is remote

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Handle autoIncrement != true

Do not ensure `appVersionSource` is set if the user has `autoIncrement: 'veresion'`. Display a message if the user doesn't have `autoIncrement: true` but don't prompt them to set `appVersionSource`

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Add missing quote

Added missing quote sign in the log message

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Add comma

Added comma to the log message

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Tweak message

Tweaked the log message according to the review comment

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* [eas-cli] Don't change eas.json in non-interactive mode

When defaulting the value of `appVersionSource` for commands executed in non-interactive mode the eas.json is not modified and the value is assumed only at runtime. This removes the need to commit changes, which in turn eliminates the risk of mistakenly committing working changes the user might have when executing the command

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update message

Updated the message to correctly state how the version are handled with local appversionsource

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update message

Updated the message to correctly state how the version are handled with remote appversionsource

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update message

Updated the message based on review suggestions

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* [eas-cli] Reformat

Improved formatting

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

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

Co-authored-by: Szymon Dziedzic <[email protected]>

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

Co-authored-by: Szymon Dziedzic <[email protected]>

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* [eas-cli] Reformat

Improved formatting

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* [eas-cli] Reformat

Improved formatting

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* [eas-cli] Post-merge fix

Fixed issues after merging in the main branch

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

* [eas-cli] Update version mentioned

Updated the version number that includes the current changes after a new major version had been released in the meantime

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

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

Co-authored-by: Szymon Dziedzic <[email protected]>

* [eas-cli] Lint

Fixed linting error

See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote

---------

Co-authored-by: Szymon Dziedzic <[email protected]>
2024-09-03 14:12:12 +02:00
Expo Bot fbc39240a5 Upgrade eas-build dependencies (#2531)
⬆️ Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.
2024-09-02 19:32:08 +02:00
Szymon Dziedzic f31054c5a5 [eas-cli] add new entitlements added after Xcode 15.0+ (#2525)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

companion to https://github.com/expo/third-party/pull/104

# How

This is my first PR adding new capabilities so please double-check if I did everything correctly, I based it on 6389f52

I got new entitlements from https://developer.apple.com/documentation/bundleresources/entitlements?changes=latest_major and added all available capabilities corresponding to these.

# Test Plan

Run prod build manually, add new entitlements for new capabilities to app config, and verify that they were enabled on the provisioning profile
2024-09-02 15:51:01 +02:00
Expo CI 7af8b62a7b update CHANGELOG.md 2024-08-31 09:25:14 +00:00
Szymon Dziedzic 56d492cbac v11.0.3 2024-08-31 11:20:03 +02:00
Szymon Dziedzic 4f53ce353c Revert "[eas-cli] Upgrade packages to SDK 51 release (#2498)" (#2524)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C02123T524U/p1724909908574979

It seems like SDK 51 config-related packages update broke builds for older SDKs

# How

I believe we didn't add any new config-related features in SDK 51, so resolving the config using the package version for SDK 50 should be ok for now and shouldn't cause any trouble.

Revert #2498

# Test Plan

Tests
2024-08-31 11:18:52 +02:00
Expo CI 736552b358 update CHANGELOG.md 2024-08-27 13:29:59 +00:00
Szymon Dziedzic b4a3f5b526 v11.0.2 2024-08-27 15:24:27 +02:00
Szymon Dziedzic 28592f27fa [eas-cli] remove unnecessary static project config context definition from eas build command (#2521)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://github.com/expo/eas-cli/issues/2519

I was able to reproduce the issue and it is indeed true. It seems like we mistakenly added a static project config definition to the `eas build` command context in https://github.com/expo/eas-cli/commit/56510f092527e458aa9dca19feac0b5d425dac56#diff-cd2e00f4d286a4714f8736e14c635f4c3b98c402367cd71a83e4cd743034bd79. The output from it wasn't really used anywhere, but adding it caused the app configs to be evaluated without `buildProfile.env` in the first pass when trying the resolve project ID and slug at the begging of command execution and our slug/project ID assertions to fail. It only affected people with dynamic project IDs and slug fields.

# How

Remove unnecessary static context from context definitions, therefore avoid resolving project ID and slug from app config without env vars as a first thing when the `eas build` command is executed.

# Test Plan

Tested manually
2024-08-27 15:23:15 +02:00
118 changed files with 14306 additions and 2021 deletions

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
rules:
- patterns: ['**/*']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/build/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/commands/build/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/submit/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/commands/submit.ts']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela', 'radoslawkrzemien']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['EvanBacon', 'byCedric', 'wschurman']
- patterns: ['packages/eas-cli/src/commands/update/**']
mentions: ['EvanBacon', 'byCedric', 'wschurman']
- patterns: ['packages/eas-cli/schema/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/commands/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-json/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
-48
View File
@@ -1,48 +0,0 @@
name: Changelog bot
on:
issue_comment:
types: [created]
jobs:
pr-commented:
runs-on: ubuntu-latest
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: 20
- name: Checkout Pull Request
run: gh pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Add changelog entry
working-directory: ./scripts
run: |
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
if [[ ! "$PARAMS" =~ " " ]]; then
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
PR_COMMENT_BODY: ${{ github.event.comment.body }}
- name: Commit and push
run: |
# use the last commit author name and email
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
+1 -2
View File
@@ -36,6 +36,5 @@ jobs:
refresh-message-position: true
message: ${{ contains(github.event.pull_request.labels.*.name, 'no changelog') && '⏩ The changelog entry check has been skipped since the "no changelog" label is present.' || '✅ Thank you for adding the changelog entry!' }}
message-failure: |
❌ It looks like a changelog entry is missing for this PR. You have two options: you can add it manually, or you can use the changelog bot to do it for you.
🤖 To use the bot, simply comment on this PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`.
❌ It looks like a changelog entry is missing for this PR. Add it manually to CHANGELOG.md.
⏩ If this PR doesn't require a changelog entry, such as if it's an internal change that doesn't affect the user experience, you can add the "no changelog" label to the PR.
+14
View File
@@ -0,0 +1,14 @@
name: codemention
on:
pull_request_target:
types: [opened, synchronize, ready_for_review]
jobs:
codemention:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: tobyhs/[email protected]
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
+108
View File
@@ -10,8 +10,116 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Disallow republishing an update that is being rolled-out. ([#2602](https://github.com/expo/eas-cli/pull/2602) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Implement new `worker` deploy API flow. ([#2601](https://github.com/expo/eas-cli/pull/2601) by [@kitten](https://github.com/kitten)))
- Unhide rollout-percentage flag on update publish command. ([#2608](https://github.com/expo/eas-cli/pull/2608) by [@wschurman](https://github.com/wschurman))
- Let folks know about the new concurrency add-on ([#2611](https://github.com/expo/eas-cli/pull/2611) by [@brentvatne](https://github.com/brentvatne))
## [12.5.1](https://github.com/expo/eas-cli/releases/tag/v12.5.1) - 2024-09-27
### 🐛 Bug fixes
- Support assigning dev domain names in non interactive mode. ([#2595](https://github.com/expo/eas-cli/pull/2595) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Simplify the output of `eas deploy --json`. ([#2596](https://github.com/expo/eas-cli/pull/2596) by [@byCedric](https://github.com/byCedric))
- Support deploying Expo Router server exports without client directory. ([#2597](https://github.com/expo/eas-cli/pull/2597) by [@byCedric](https://github.com/byCedric))
- Add exit option to `eas credentials` interactive menu. ([#2570](https://github.com/expo/eas-cli/pull/2570) by [@szdziedzic](https://github.com/szdziedzic))
## [12.5.0](https://github.com/expo/eas-cli/releases/tag/v12.5.0) - 2024-09-23
### 🎉 New features
- Log command execution to assist in debugging local builds. ([#2526](https://github.com/expo/eas-cli/pull/2526) by [@trajano](https://github.com/trajano))
- Allow submitting builds in progress ([#2543](https://github.com/expo/eas-cli/pull/2543) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Use `EAS_DANGEROUS_OVERRIDE_ANDROID_APPLICATION_ID` and `EAS_DANGEROUS_OVERRIDE_IOS_BUNDLE_IDENTIFIER` environment variables as overrides of the Android application ID and iOS bundle identifier in managed workflow too. ([#2576](https://github.com/expo/eas-cli/pull/2576) by [@sjchmiela](https://github.com/sjchmiela))
- Add destination branch arguments to update:republish command. ([#2575](https://github.com/expo/eas-cli/pull/2575) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Avoid malforming `app.json` with empty `.expo` object. ([#2573](https://github.com/expo/eas-cli/pull/2573) by [@byCedric](https://github.com/byCedric))
- Fix typo causing `worker:deploy` asset upload errors not to be shown properly. ([#2579](https://github.com/expo/eas-cli/pull/2579) by [@kitten](https://github.com/kitten))
## [12.4.1](https://github.com/expo/eas-cli/releases/tag/v12.4.1) - 2024-09-14
## [12.4.0](https://github.com/expo/eas-cli/releases/tag/v12.4.0) - 2024-09-14
### 🎉 New features
- Add `worker:alias` command to assign aliases from the CLI. ([#2548](https://github.com/expo/eas-cli/pull/2548) by [@byCedric](https://github.com/byCedric))
- Add `worker --prod` flag to deploy to production from the CLI. ([#2550](https://github.com/expo/eas-cli/pull/2550) by [@byCedric](https://github.com/byCedric))
- Add `worker --alias` flag to assign custom aliases when deploying. ([#2551](https://github.com/expo/eas-cli/pull/2551) by [@byCedric](https://github.com/byCedric)))
- Add `worker --id` flag to use a custom deployment identifier. ([#2552](https://github.com/expo/eas-cli/pull/2552) by [@byCedric](https://github.com/byCedric)))
- Add `worker --environment` flag to deploy with EAS environment variables. ([#2557](https://github.com/expo/eas-cli/pull/2557) by [@kitten](https://github.com/kitten)))
- Add `worker --export-dir` flag to select exported directory. ([#2560](https://github.com/expo/eas-cli/pull/2560) by [@byCedric](https://github.com/byCedric)))
- Add `worker --json` flag to allow integrating with 3rd parties and custom tooling. ([#2561](https://github.com/expo/eas-cli/pull/2561) by [@byCedric](https://github.com/byCedric)))
- Add `worker:alias --json` flag to allow integrating with 3rd parties and custom tooling. ([#2562](https://github.com/expo/eas-cli/pull/2562) by [@byCedric](https://github.com/byCedric)))
### 🐛 Bug fixes
- Avoid merging `expo.extra` plugin-generated data with `expo.extra.eas.projectId` in `eas init`. ([#2554](https://github.com/expo/eas-cli/pull/2554) by [@byCedric](https://github.com/byCedric)))
- Restore "export not found" error and hide recent export timestamps. ([#2566](https://github.com/expo/eas-cli/pull/2566) by [@byCedric](https://github.com/byCedric)))
- Check if export is available before validating project ID in `eas worker`. ([#2569](https://github.com/expo/eas-cli/pull/2569) by [@byCedric](https://github.com/byCedric)))
### 🧹 Chores
- Make error message for invalid CFBundleShortVersionString more descriptive and actionable. Improve CFBundleShortVersionString validation regex. ([#2542](https://github.com/expo/eas-cli/pull/2542) by [@szdziedzic](https://github.com/szdziedzic))
- Add missing `--non-interactive` argument to `worker:deploy` command. ([#2544](https://github.com/expo/eas-cli/pull/2544) by [@kitten](https://github.com/kitten))
- Source `@expo/env` dotenv files for worker deployments. ([#2545](https://github.com/expo/eas-cli/pull/2545) by [@kitten](https://github.com/kitten))
- Support `worker --production` and clean up command output. ([#2555](https://github.com/expo/eas-cli/pull/2555) by [@byCedric](https://github.com/byCedric)))
- Unify both `worker` and `worker:alias` command output. ([#2558](https://github.com/expo/eas-cli/pull/2558) by [@byCedric](https://github.com/byCedric)))
- Share similar table/json output in both `worker` and `worker:alias` command outputs. ([#2563](https://github.com/expo/eas-cli/pull/2563) by [@byCedric](https://github.com/byCedric)))
- Polish the project URL prompt when setting up new projects. ([#2564](https://github.com/expo/eas-cli/pull/2564) by [@byCedric](https://github.com/byCedric)))
- Always assume `static` exports in `eas deploy` and add modified time. ([#2565](https://github.com/expo/eas-cli/pull/2565) by [@byCedric](https://github.com/byCedric)))
- Update the `eas worker --help` `--environment` description. ([#2567](https://github.com/expo/eas-cli/pull/2567) by [@byCedric](https://github.com/byCedric)))
- Remove the cursor space after selecting project dev domain. ([#2568](https://github.com/expo/eas-cli/pull/2568) by [@byCedric](https://github.com/byCedric)))
- Reword the dev domain prompt to mention "preview URL". ([#2572](https://github.com/expo/eas-cli/pull/2572) by [@byCedric](https://github.com/byCedric)))
## [12.3.0](https://github.com/expo/eas-cli/releases/tag/v12.3.0) - 2024-09-09
### 🎉 New features
- Add `--non-interactive` and `--force` support when `--id` is not passed to the `eas init` command. ([#1983](https://github.com/expo/eas-cli/pull/1983) by [@mymattcarroll](https://github.com/mymattcarroll))
## [12.1.1](https://github.com/expo/eas-cli/releases/tag/v12.1.1) - 2024-09-09
### 🐛 Bug fixes
- Fixed an issue where extensions retrieved the main app's entitlements instead of their own. ([#2532](https://github.com/expo/eas-cli/pull/2532) by [@HarrisHan](https://github.com/HarrisHan))
## [12.1.0](https://github.com/expo/eas-cli/releases/tag/v12.1.0) - 2024-09-06
### 🎉 New features
- **Internal/Experimental:** Add EAS Worker command ([#2447](https://github.com/expo/eas-cli/pull/2447) by [@kitten](https://github.com/kitten))
- Upload fingeprint source as part of eas update command. ([#2533](https://github.com/expo/eas-cli/pull/2533) by [@wschurman](https://github.com/wschurman))
## [12.0.0](https://github.com/expo/eas-cli/releases/tag/v12.0.0) - 2024-09-04
### 🛠 Breaking changes
- Prompt the users to set `appVersionSource`, while mentioning that `remote` is the default. ([#2411](https://github.com/expo/eas-cli/pull/2411) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
### 🎉 New features
- Add support for syncing Journaling Suggestions, Managed App Installation UI, and 5G Network Slicing capabilities. ([#2525](https://github.com/expo/eas-cli/pull/2525) by [@szdziedzic](https://github.com/szdziedzic))
## [11.0.3](https://github.com/expo/eas-cli/releases/tag/v11.0.3) - 2024-08-31
### 🐛 Bug fixes
- Revert config-related packages to SDK 50 version from SDK 51 version to fix broken entitlements behavior for older SDKs. ([#2524](https://github.com/expo/eas-cli/pull/2524) by [@szdziedzic](https://github.com/szdziedzic))
## [11.0.2](https://github.com/expo/eas-cli/releases/tag/v11.0.2) - 2024-08-27
### 🐛 Bug fixes
- Remove unncessary static project config context definition from `eas build` command context, to fix resolving dynamic projest ID and slug for dynamic app configs. ([#2521](https://github.com/expo/eas-cli/pull/2521) by [@szdziedzic](https://github.com/szdziedzic))
## [11.0.1](https://github.com/expo/eas-cli/releases/tag/v11.0.1) - 2024-08-26
### 🐛 Bug fixes
-16
View File
@@ -1,16 +0,0 @@
* @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/submit @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/submit.ts @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric
packages/eas-json @szdziedzic @khamilowicz @sjchmiela
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "11.0.1",
"version": "12.5.2",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+78 -68
View File
@@ -143,7 +143,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -160,7 +160,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -177,7 +177,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -191,7 +191,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -243,7 +243,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -264,7 +264,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -284,7 +284,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -304,7 +304,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -327,7 +327,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -364,7 +364,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -383,7 +383,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -400,7 +400,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -419,7 +419,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:inspect`
@@ -454,7 +454,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -504,7 +504,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -533,7 +533,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -559,7 +559,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -609,7 +609,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -628,7 +628,7 @@ DESCRIPTION
update version of an app
```
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -647,7 +647,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -664,7 +664,7 @@ DESCRIPTION
view a build for your project
```
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -685,7 +685,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -707,7 +707,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -727,7 +727,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -760,7 +760,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -783,7 +783,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -804,7 +804,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -821,7 +821,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -839,7 +839,7 @@ DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas device:create`
@@ -853,7 +853,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -873,7 +873,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -894,7 +894,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -915,7 +915,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -929,7 +929,7 @@ DESCRIPTION
view a device for your project
```
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -943,7 +943,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -971,10 +971,11 @@ create or link an EAS project
```
USAGE
$ eas init [--force --id <value>] [--non-interactive ]
$ eas init [--id <value>] [--force] [--non-interactive]
FLAGS
--force Whether to overwrite any existing project ID
--force Whether to create a new project/link an existing project without additional prompts or overwrite
any existing project ID when running with --id flag
--id=<value> ID of the EAS project to link
--non-interactive Run the command in non-interactive mode.
@@ -1050,7 +1051,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1067,7 +1068,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1084,7 +1085,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1114,7 +1115,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1128,7 +1129,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1136,10 +1137,11 @@ create or link an EAS project
```
USAGE
$ eas project:init [--force --id <value>] [--non-interactive ]
$ eas project:init [--id <value>] [--force] [--non-interactive]
FLAGS
--force Whether to overwrite any existing project ID
--force Whether to create a new project/link an existing project without additional prompts or overwrite
any existing project ID when running with --id flag
--id=<value> ID of the EAS project to link
--non-interactive Run the command in non-interactive mode.
@@ -1150,7 +1152,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1168,7 +1170,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas secret:create`
@@ -1191,7 +1193,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [packages/eas-cli/src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [packages/eas-cli/src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1209,7 +1211,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [packages/eas-cli/src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [packages/eas-cli/src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1223,7 +1225,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [packages/eas-cli/src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [packages/eas-cli/src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1243,7 +1245,7 @@ DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [packages/eas-cli/src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [packages/eas-cli/src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -1274,7 +1276,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1283,8 +1285,8 @@ publish an update group
```
USAGE
$ eas update [--branch <value>] [--channel <value>] [-m <value>] [--input-dir <value>] [--skip-bundler]
[--clear-cache] [--emit-metadata] [-p android|ios|all] [--auto] [--private-key-path <value>] [--json
--non-interactive]
[--clear-cache] [--emit-metadata] [--rollout-percentage <value>] [-p android|ios|all] [--auto] [--private-key-path
<value>] [--json --non-interactive]
FLAGS
-m, --message=<value> A short message describing the update
@@ -1302,13 +1304,17 @@ FLAGS
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory. Only relevant if you are using code signing:
https://docs.expo.dev/eas-update/code-signing/
--rollout-percentage=<value> Percentage of users this update should be immediately available to. Users not in the
rollout will be served the previous latest update on the branch, even if that update
is itself being rolled out. The specified number must be an integer between 1 and
100. When not specified, this defaults to 100.
--skip-bundler Skip running Expo CLI to bundle the app before publishing
DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1326,7 +1332,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1347,7 +1353,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1369,7 +1375,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1377,14 +1383,18 @@ roll back to an existing update
```
USAGE
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [-m <value>] [-p android|ios|all]
[--private-key-path <value>] [--json --non-interactive]
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [--destination-channel <value> |
--destination-branch <value>] [-m <value>] [-p android|ios|all] [--private-key-path <value>] [--json
--non-interactive]
FLAGS
-m, --message=<value> Short message describing the republished update group
-p, --platform=(android|ios|all) [default: all]
--branch=<value> Branch name to select an update group to republish from
--channel=<value> Channel name to select an update group to republish from
--destination-branch=<value> Branch name to republish to if republishing to a different branch
--destination-channel=<value> Channel name to select a branch to republish to if republishing to a different
branch
--group=<value> Update group ID to republish
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
@@ -1397,7 +1407,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1425,7 +1435,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1445,7 +1455,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1465,7 +1475,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1486,7 +1496,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1506,7 +1516,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1524,7 +1534,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1546,7 +1556,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1563,7 +1573,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v11.0.1/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v12.5.2/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+1
View File
@@ -7,6 +7,7 @@ documents:
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
- 'src/worker/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
+6165 -622
View File
File diff suppressed because it is too large. Load diff
+22 -17
View File
@@ -1,34 +1,35 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "11.0.1",
"version": "12.5.2",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "1.7.1",
"@expo/apple-utils": "1.8.0",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "9.0.3",
"@expo/config-plugins": "8.0.8",
"@expo/config-types": "51.0.2",
"@expo/eas-build-job": "1.0.133",
"@expo/eas-json": "11.0.0",
"@expo/json-file": "8.3.3",
"@expo/config": "8.5.4",
"@expo/config-plugins": "7.8.4",
"@expo/config-types": "50.0.0",
"@expo/eas-build-job": "1.0.136",
"@expo/eas-json": "12.5.2",
"@expo/env": "^0.3.0",
"@expo/json-file": "8.2.37",
"@expo/logger": "1.0.117",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.1.3",
"@expo/package-manager": "1.5.2",
"@expo/pkcs12": "0.1.2",
"@expo/plist": "0.1.3",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "1.1.2",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.20",
"@expo/plugin-help": "5.1.23",
"@expo/plugin-warn-if-update-available": "2.5.1",
"@expo/prebuild-config": "7.0.8",
"@expo/prebuild-config": "6.7.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.130",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.136",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -63,6 +64,7 @@
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.1.2",
"minizlib": "3.0.1",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
@@ -78,12 +80,14 @@
"semver": "7.5.4",
"slash": "3.0.0",
"tar": "6.2.1",
"tar-stream": "3.1.7",
"terminal-link": "2.1.1",
"tslib": "2.6.2",
"turndown": "7.1.2",
"untildify": "4.0.0",
"uuid": "9.0.1",
"wrap-ansi": "7.0.0"
"wrap-ansi": "7.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
@@ -105,12 +109,13 @@
"@types/prompts": "2.4.2",
"@types/semver": "7.5.6",
"@types/tar": "6.1.10",
"@types/tar-stream": "3.1.3",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "9.0.7",
"@types/wrap-ansi": "3.0.0",
"axios": "0.28.1",
"eslint-plugin-graphql": "4.0.0",
"express": "4.19.2",
"express": "4.20.0",
"memfs": "3.4.13",
"mockdate": "3.0.5",
"nock": "13.4.0",
@@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BuildVersionGetView reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL 1`] = `"Aborting..."`;
exports[`BuildVersionGetView reading version when appVersionSource is set to local 1`] = `"This project is not configured for using remote version source. Add {"cli": { "appVersionSource": "remote" }} in eas.json or re-run this command without "--non-interactive" flag."`;
@@ -1,10 +1,18 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils';
import {
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import BuildVersionGetView from '../../commands/build/version/get';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
jest.mock('../../project/applicationIdentifier');
@@ -12,16 +20,7 @@ jest.mock('fs');
jest.mock('../../log');
jest.mock('../../utils/json');
jest.mock('../../graphql/queries/AppVersionQuery');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
jest.mock('../../prompts');
describe(BuildVersionGetView, () => {
afterEach(() => {
@@ -49,6 +48,29 @@ describe(BuildVersionGetView, () => {
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('reading version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(Log.log).toHaveBeenCalledWith(`Android versionCode - ${chalk.bold('100')}`);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('reading version for platform android when no remote version is set', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
@@ -145,7 +167,9 @@ describe(BuildVersionGetView, () => {
});
test('reading version when appVersionSource is set to local ', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
@@ -158,4 +182,51 @@ describe(BuildVersionGetView, () => {
);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
test('reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
test('reading version aborts when the appVersionSource is not specified and the user chooses to configure it manually', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
});
test('reading version sets appVersionSource to LOCAL and aborts when the appVersionSource is not specified and is run in non-interactive mode', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(
BuildVersionGetView,
['--non-interactive', '--json', '--platform=android'],
ctx
);
await expect(cmd.run()).rejects.toThrowError(
`This project is not configured for using remote version source. Add ${chalk.bold(
'{"cli": { "appVersionSource": "remote" }}'
)} in eas.json or re-run this command without "--non-interactive" flag.`
);
});
});
@@ -1,4 +1,3 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
@@ -8,12 +7,15 @@ import {
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import BuildVersionSetView from '../../commands/build/version/set';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts';
jest.mock('../../project/applicationIdentifier');
@@ -25,16 +27,6 @@ jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSetView, () => {
afterEach(() => {
jest.clearAllMocks();
@@ -67,6 +59,34 @@ describe(BuildVersionSetView, () => {
);
});
test('setting version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
version: '1000',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(AppVersionMutation.createAppVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
expect.objectContaining({
buildVersion: '1000',
storeVersion: '1.0.0',
})
);
});
test('printing current remote version before prompting for a new one', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withRemoteVersionSource(getMockEasJson()),
@@ -115,7 +135,9 @@ describe(BuildVersionSetView, () => {
});
test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
@@ -125,10 +147,26 @@ describe(BuildVersionSetView, () => {
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('setting version when appVersionSource is set to local and user allows auto configuration', async () => {
test('setting version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => false);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborting...');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('setting version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
@@ -137,4 +175,33 @@ describe(BuildVersionSetView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('setting version when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they allow auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => true);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await cmd.run();
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('setting version aborts when the appVersionSource is not specified and the user chooses to configure manually', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
});
@@ -1,5 +1,4 @@
import { Workflow } from '@expo/eas-build-job';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
@@ -9,6 +8,8 @@ import {
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/version';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../build/ios/version';
@@ -19,6 +20,7 @@ import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import { getAppBuildGradleAsync } from '../../project/android/gradleUtils';
import { resolveTargetsAsync } from '../../project/ios/target';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import { resolveWorkflowAsync } from '../../project/workflow';
import * as prompts from '../../prompts';
@@ -37,16 +39,6 @@ jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSyncView, () => {
afterEach(() => {
jest.clearAllMocks();
@@ -79,6 +71,34 @@ describe(BuildVersionSyncView, () => {
expect(syncAndroidAsync).not.toHaveBeenCalled();
});
test('syncing version for managed project on platform android when appVersionSource is not set and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '1000',
storeVersion: '0.0.1',
}));
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
version: '1000',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any);
await cmd.run();
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'ANDROID',
'eas.test.com'
);
expect(syncAndroidAsync).not.toHaveBeenCalled();
});
test('syncing version for bare project on platform android', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withRemoteVersionSource(getMockEasJson()),
@@ -179,7 +199,9 @@ describe(BuildVersionSyncView, () => {
});
test('syncing version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
@@ -189,10 +211,26 @@ describe(BuildVersionSyncView, () => {
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
test('syncing version aborts when appVersionSource is not set and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborting...');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
@@ -201,4 +239,33 @@ describe(BuildVersionSyncView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('syncing version when appVersionSource is not set and the user chooses to set it to LOCAL and they allow auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await cmd.run();
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('syncing version aborts when appVersionSource is not set and the user chooses to configure manually', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
});
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config-types';
import { EasJson } from '@expo/eas-json';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import { Command, Config } from '@oclif/core';
import { vol } from 'memfs';
import path from 'path';
@@ -148,3 +148,23 @@ export const getError = <TError = any>(call: () => unknown): TError | NoErrorThr
return error as TError;
}
};
export function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
export function withLocalVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.LOCAL,
},
};
}
@@ -0,0 +1,77 @@
import { AppVersionSource, EasJsonAccessor } from '@expo/eas-json';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { easCliVersion } from '../../utils/easCli';
import GitClient from '../../vcs/clients/git';
import { Client } from '../../vcs/vcs';
import { ensureProjectConfiguredAsync } from '../configure';
jest.mock('fs');
jest.mock('../../vcs/vcs');
jest.mock('../../vcs/clients/git');
beforeEach(async () => {
vol.reset();
});
describe(ensureProjectConfiguredAsync, () => {
it('returns false and does not configure if eas.json exists', async () => {
vol.fromJSON({
'./eas.json': JSON.stringify({
cli: {
version: `>= ${easCliVersion}`,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
},
}),
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const vcsClientMock = jest.mocked(new GitClient());
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
const result = await ensureProjectConfiguredAsync({
projectDir: '.',
nonInteractive: false,
vcsClient: vcsClientMock as unknown as Client,
});
expect(result).toBeFalsy();
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
});
it('returns true and configures if eas.json does not exist', async () => {
const writeFileMock = jest.spyOn(fs, 'writeFile');
writeFileMock.mockImplementation(async (...args) => {
const easJsonPath = args[0] as string;
const easJsonData = args[1] as string;
vol.fromJSON({
[easJsonPath]: easJsonData,
});
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeFalsy();
const vcsClientMock = jest.mocked(new GitClient());
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
const result = await ensureProjectConfiguredAsync({
projectDir: '.',
nonInteractive: false,
vcsClient: vcsClientMock as unknown as Client,
});
expect(result).toBeTruthy();
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const easJson = await EasJsonAccessor.fromProjectPath('.').readAsync();
expect(easJson.cli?.appVersionSource).toEqual(AppVersionSource.REMOTE);
});
});
+1 -1
View File
@@ -107,7 +107,7 @@ export async function prepareAndroidBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
env: ctx.env,
});
},
prepareJobAsync: async (
+17 -65
View File
@@ -2,7 +2,7 @@ import {
ArchiveSource,
ArchiveSourceType,
BuildJob,
FingerprintSourceType,
FingerprintSource,
Metadata,
Platform,
} from '@expo/eas-build-job';
@@ -13,8 +13,6 @@ import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { BuildContext } from './context';
import {
@@ -64,11 +62,11 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAsync';
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { getTmpDirectory } from '../utils/paths';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -186,7 +184,7 @@ export async function prepareBuildRequestForPlatformAsync<
return async () => {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions, ctx.env);
return undefined;
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
@@ -488,8 +486,8 @@ async function handleSingleBuildProgressAsync(
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New:
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
formatAccountSubscriptionsUrl(accountName)
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link(
formatAccountBillingUrl(accountName)
)}.`;
break;
case BuildStatus.InQueue: {
@@ -511,7 +509,7 @@ async function handleSingleBuildProgressAsync(
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${link(
formatAccountSubscriptionsUrl(accountName)
formatAccountBillingUrl(accountName)
)}`
);
}
@@ -658,25 +656,22 @@ function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
}
}
function formatAccountSubscriptionsUrl(accountName: string): string {
return new URL(
`/accounts/${accountName}/settings/subscriptions`,
getExpoWebsiteBaseUrl()
).toString();
function formatAccountBillingUrl(accountName: string): string {
return new URL(`/accounts/${accountName}/settings/billing`, getExpoWebsiteBaseUrl()).toString();
}
async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{
runtimeVersion?: string;
fingerprintSource?: Metadata['fingerprintSource'];
fingerprintSource?: FingerprintSource;
}> {
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
exp: ctx.exp,
platform: ctx.platform,
workflow: ctx.workflow,
projectDir: ctx.projectDir,
env: ctx.buildProfile.env,
env: ctx.env,
cwd: ctx.projectDir,
});
@@ -685,62 +680,19 @@ async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
}
/**
* It's ok for fingerprintSources to be empty
* It's ok for fingerprintSources or runtimeVersion to be empty
* fingerprintSources only exist if the project is using runtimeVersion.policy: fingerprint
*/
if (!resolvedRuntimeVersion.fingerprint) {
if (!resolvedRuntimeVersion.fingerprint || !resolvedRuntimeVersion.runtimeVersion) {
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
};
}
await fs.mkdirp(getTmpDirectory());
const fingerprintLocation = path.join(getTmpDirectory(), `${uuidv4()}-runtime-fingerprint.json`);
await fs.writeJSON(fingerprintLocation, {
hash: resolvedRuntimeVersion.runtimeVersion,
sources: resolvedRuntimeVersion.fingerprint.fingerprintSources,
return await maybeUploadFingerprintAsync({
runtimeVersion: resolvedRuntimeVersion.runtimeVersion,
fingerprint: resolvedRuntimeVersion.fingerprint,
graphqlClient: ctx.graphqlClient,
localBuildMode: ctx.localBuildOptions.localBuildMode,
});
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
fingerprintSource: {
type: FingerprintSourceType.PATH,
path: fingerprintLocation,
isDebugFingerprint: resolvedRuntimeVersion.fingerprint.isDebugFingerprintSource,
},
};
}
let fingerprintGCSBucketKey = undefined;
try {
fingerprintGCSBucketKey = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasUpdateFingerprint,
fingerprintLocation
);
} catch (err: any) {
let errMessage = 'Failed to upload fingerprint to EAS';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
};
} finally {
await fs.remove(fingerprintLocation);
}
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
fingerprintSource: {
type: FingerprintSourceType.GCS,
bucketKey: fingerprintGCSBucketKey,
isDebugFingerprint: resolvedRuntimeVersion.fingerprint.isDebugFingerprintSource,
},
};
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import { AppVersionSource, EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -55,6 +55,7 @@ async function configureAsync({
const EAS_JSON_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
appVersionSource: AppVersionSource.REMOTE,
},
build: {
development: {
@@ -64,7 +65,9 @@ const EAS_JSON_DEFAULT: EasJson = {
preview: {
distribution: 'internal',
},
production: {},
production: {
autoIncrement: true,
},
},
submit: {
production: {},
+1
View File
@@ -63,4 +63,5 @@ export interface BuildContext<T extends Platform> {
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
env: Record<string, string>;
}
+7 -2
View File
@@ -43,6 +43,7 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel,
freezeCredentials,
repack,
env,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -64,8 +65,11 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env,
});
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
@@ -87,7 +91,7 @@ export async function createBuildContextAsync<T extends Platform>({
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
env,
easJsonCliConfig,
vcsClient,
freezeCredentials,
@@ -147,6 +151,7 @@ export async function createBuildContextAsync<T extends Platform>({
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
env,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -0,0 +1,109 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log, { learnMore } from '../log';
function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export async function evaluateConfigWithEnvVarsAsync<Config extends { projectId: string }, Opts>({
buildProfile,
buildProfileName,
graphqlClient,
getProjectConfig,
opts,
}: {
buildProfile: BuildProfile;
buildProfileName: string;
graphqlClient: ExpoGraphqlClient | null;
opts: Opts;
getProjectConfig(opts: Opts): Promise<Config>;
}): Promise<Config & { env: Env }> {
if (!graphqlClient) {
Log.warn('An Expo user account is required to fetch environment variables.');
const config = await getProjectConfig(opts);
return { env: buildProfile.env ?? {}, ...config };
}
const { projectId } = await getProjectConfig({ env: buildProfile.env, ...opts });
const env = await resolveEnvVarsAsync({
buildProfile,
buildProfileName,
graphqlClient,
projectId,
});
const config = await getProjectConfig({ ...opts, env });
return { env, ...config };
}
async function resolveEnvVarsAsync({
buildProfile,
buildProfileName,
graphqlClient,
projectId,
}: {
buildProfile: BuildProfile;
buildProfileName: string;
graphqlClient: ExpoGraphqlClient;
projectId: string;
}): Promise<Env> {
const environment =
buildProfile.environment?.toUpperCase() ?? process.env.EAS_CURRENT_ENVIRONMENT;
if (!environment || !isEnvironment(environment)) {
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
buildProfile.env && Object.keys(buildProfile.env).length > 0
? Object.keys(buildProfile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
return { ...buildProfile.env };
}
try {
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
environment,
}
);
const serverEnvVars = Object.fromEntries(
environmentVariables
.filter(({ name, value }) => name && value)
.map(({ name, value }) => [name, value])
) as Record<string, string>;
const envVarsWithSource: Record<string, 'build profile' | 'EAS server'> = {
...Object.fromEntries(Object.keys(serverEnvVars).map(key => [key, 'EAS server'])),
...(buildProfile.env
? Object.fromEntries(Object.keys(buildProfile.env).map(key => [key, 'build profile']))
: null),
};
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile and "${environment.toLowerCase()}" environment: ${
Object.keys(envVarsWithSource).length > 0
? Object.keys(envVarsWithSource)
.map(key => `${key} (source: ${envVarsWithSource[key]})`)
.join('\n')
: 'no environment variables specified'
}\n${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
return { ...serverEnvVars, ...buildProfile.env };
} catch (e) {
Log.error(`Failed to pull env variables for environment ${environment} from EAS servers`);
Log.error(e);
Log.error(
'This can possibly be a bug in EAS/EAS CLI. Report it here: https://github.com/expo/eas-cli/issues'
);
return { ...buildProfile.env };
}
}
+19 -1
View File
@@ -3,6 +3,7 @@ import {
ArchiveSourceType,
BuildMode,
BuildTrigger,
FingerprintSourceType,
Metadata,
Workflow,
} from '@expo/eas-build-job';
@@ -15,8 +16,10 @@ import {
BuildMetadataInput,
BuildWorkflow,
DistributionType,
FingerprintSourceInput,
BuildMode as GraphQLBuildMode,
BuildTrigger as GraphQLBuildTrigger,
FingerprintSourceType as GraphQLFingeprintSourceType,
ProjectArchiveSourceInput,
ProjectArchiveSourceType,
WorkerLoggerLevel,
@@ -42,7 +45,8 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return {
...metadata,
fingerprintSource: metadata.fingerprintSource as BuildMetadataInput['fingerprintSource'],
fingerprintSource:
metadata.fingerprintSource && transformFingerprintSource(metadata.fingerprintSource),
credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
@@ -53,6 +57,20 @@ export function transformMetadata(metadata: Metadata): BuildMetadataInput {
};
}
export function transformFingerprintSource(
fingerprintSource: NonNullable<Metadata['fingerprintSource']>
): FingerprintSourceInput | null {
if (fingerprintSource.type !== FingerprintSourceType.GCS) {
return null;
}
return {
type: GraphQLFingeprintSourceType.Gcs,
bucketKey: fingerprintSource.bucketKey,
isDebugFingerprint: fingerprintSource.isDebugFingerprint,
};
}
function transformCredentialsSource(
credentialsSource: Metadata['credentialsSource']
): BuildCredentialsSource {
@@ -5,6 +5,7 @@ import { vol } from 'memfs';
import os from 'os';
import type { XCBuildConfiguration } from 'xcode';
import { learnMore } from '../../../log';
import { readPlistAsync } from '../../../utils/plist';
import { resolveVcsClient } from '../../../vcs';
import {
@@ -316,7 +317,9 @@ describe(readShortVersionAsync, () => {
vcsClient
)
).rejects.toThrowError(
'CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: 0.0.7.1.028'
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: 0.0.7.1.028. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
});
});
+3 -3
View File
@@ -24,7 +24,7 @@ import {
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> {
const { buildProfile } = ctx;
const { buildProfile, env } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
@@ -47,7 +47,7 @@ export async function createIosContextAsync(
projectDir: ctx.projectDir,
exp: ctx.exp,
xcodeBuildContext,
env: buildProfile.env,
env,
vcsClient: ctx.vcsClient,
});
const applicationTarget = findApplicationTarget(targets);
@@ -89,7 +89,7 @@ export async function prepareIosBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
env: ctx.env,
});
},
prepareJobAsync: async (
+24 -8
View File
@@ -11,7 +11,7 @@ import { Target } from '../../credentials/ios/types';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import Log, { learnMore } from '../../log';
import { ora } from '../../ora';
import { findApplicationTarget } from '../../project/ios/target';
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
@@ -23,7 +23,7 @@ import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
const SHORT_VERSION_REGEX = /^\d+.\d+.\d+$/;
const SHORT_VERSION_REGEX = /^\d+(\.\d+){0,2}$/;
export enum BumpStrategy {
APP_VERSION,
@@ -100,11 +100,27 @@ export async function bumpVersionInAppJsonAsync({
}
}
function validateShortVersion(shortVersion: string | undefined): void {
function validateShortVersion({
shortVersion,
workflow,
}: {
shortVersion: string | undefined;
workflow: Workflow;
}): void {
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
throw new Error(
`CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: ${shortVersion}`
);
if (workflow === Workflow.MANAGED) {
throw new Error(
`The required format for "version" field from app.json/app.config.ts is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the required format. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
} else {
throw new Error(
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
}
}
}
@@ -122,10 +138,10 @@ export async function readShortVersionAsync(
infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings);
validateShortVersion(shortVersion);
validateShortVersion({ shortVersion, workflow });
return shortVersion;
} else {
validateShortVersion(exp.version);
validateShortVersion({ shortVersion: exp.version, workflow });
return exp.version;
}
}
+22 -12
View File
@@ -3,10 +3,11 @@ import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
import Log from '../log';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.131';
const PLUGIN_PACKAGE_VERSION = '1.0.135';
export enum LocalBuildMode {
/**
@@ -39,7 +40,8 @@ export interface LocalBuildOptions {
export async function runLocalBuildAsync(
job: Job,
metadata: Metadata,
options: LocalBuildOptions
options: LocalBuildOptions,
env: Record<string, string>
): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner;
@@ -54,18 +56,26 @@ export async function runLocalBuildAsync(
};
process.on('SIGINT', interruptHandler);
try {
const mergedEnv = {
...env,
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
};
// log command execution to assist in debugging local builds
Log.debug('Running local build, using local-build-plugin', {
command,
args,
env: mergedEnv,
});
const spawnPromise = spawnAsync(command, args, {
stdio: options.verbose ? 'inherit' : 'pipe',
env: {
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
},
env: mergedEnv,
});
childProcess = spawnPromise.child;
await spawnPromise;
+42 -14
View File
@@ -2,7 +2,6 @@ import { ExpoConfig } from '@expo/config-types';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonAccessor,
EasJsonUtils,
@@ -19,6 +18,7 @@ import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './
import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
import { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync';
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
@@ -57,8 +57,9 @@ import {
validateAppVersionRuntimePolicySupportAsync,
} from '../project/projectUtils';
import {
ensureAppVersionSourceIsSetAsync,
validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettings,
validateBuildProfileVersionSettingsAsync,
} from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts';
import { runAsync } from '../run/run';
@@ -129,13 +130,6 @@ export async function runBuildAndSubmitAsync(
profileName: flags.profile ?? undefined,
projectDir,
});
Log.log(
`Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
buildProfiles[0].profile.env
? Object.keys(buildProfiles[0].profile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
for (const buildProfile of buildProfiles) {
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
@@ -173,7 +167,12 @@ export async function runBuildAndSubmitAsync(
const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } =
{};
for (const buildProfile of buildProfiles) {
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
await validateBuildProfileVersionSettingsAsync(
buildProfile,
easJsonCliConfig,
projectDir,
flags
);
const maybeMetadata = await validateCustomBuildConfigAsync({
projectDir,
profile: buildProfile.profile,
@@ -192,6 +191,15 @@ export async function runBuildAndSubmitAsync(
for (const buildProfile of buildProfiles) {
const platform = toAppPlatform(buildProfile.platform);
const { env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
flags,
@@ -204,6 +212,7 @@ export async function runBuildAndSubmitAsync(
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
env,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -251,7 +260,6 @@ export async function runBuildAndSubmitAsync(
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
selectedSubmitProfileName: flags.submitProfile,
@@ -340,6 +348,7 @@ async function prepareAndStartBuildAsync({
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
env,
}: {
projectDir: string;
flags: BuildFlags;
@@ -352,6 +361,7 @@ async function prepareAndStartBuildAsync({
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
env: Env;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
@@ -374,6 +384,7 @@ async function prepareAndStartBuildAsync({
buildLoggerLevel: flags.buildLoggerLevel,
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
env,
});
if (moreBuilds) {
@@ -412,6 +423,25 @@ async function prepareAndStartBuildAsync({
}
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
if (
easJsonCliConfig?.appVersionSource === undefined &&
buildProfile.profile.autoIncrement !== 'version'
) {
if (buildProfile.profile.autoIncrement !== true) {
Log.warn(
`The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
} else {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
easJsonCliConfig = await ensureAppVersionSourceIsSetAsync(
easJsonAccessor,
easJsonCliConfig,
flags.nonInteractive
);
}
}
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
}
@@ -448,7 +478,6 @@ async function prepareAndStartSubmissionAsync({
buildCtx,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
selectedSubmitProfileName,
nonInteractive,
@@ -457,7 +486,6 @@ async function prepareAndStartSubmissionAsync({
buildCtx: BuildContext<Platform>;
moreBuilds: boolean;
projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
selectedSubmitProfileName?: string;
nonInteractive: boolean;
@@ -469,7 +497,7 @@ async function prepareAndStartSubmissionAsync({
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
env: buildProfile.env,
env: buildCtx.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
+23
View File
@@ -1,3 +1,9 @@
import { ResourceClass } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { LocalBuildOptions } from './local';
import { RequestedPlatform } from '../platform';
export enum BuildStatus {
NEW = 'new',
IN_QUEUE = 'in-queue',
@@ -14,3 +20,20 @@ export enum BuildDistributionType {
/** @deprecated Use simulator flag instead */
SIMULATOR = 'simulator',
}
export interface BuildFlags {
requestedPlatform: RequestedPlatform;
profile?: string;
nonInteractive: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
resourceClass?: ResourceClass;
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
}
+2 -2
View File
@@ -9,9 +9,9 @@ import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image';
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
if (ctx.env?.NODE_ENV === 'production') {
Log.warn(
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
'You set NODE_ENV=production in the build profile or environment variables. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
);
Log.newLine();
}
@@ -18,13 +18,15 @@ import { Actor, getActorUsername } from '../../../user/User';
* Save an EAS project ID to the appropriate field in the app config.
*
* @deprecated Should not be used outside of context functions except in the init command.
* @deprecated Starting from `@expo/config` from SDK 52, the `modifyConfigAsync` function is merging existing data. Once this is released, we can use that instead of manually merging.
*/
export async function saveProjectIdToAppConfigAsync(
projectDir: string,
projectId: string,
options: { env?: Env } = {}
): Promise<void> {
const exp = getPrivateExpoConfig(projectDir, options);
// NOTE(cedric): we disable plugins to avoid writing plugin-generated content to `expo.extra`
const exp = getPrivateExpoConfig(projectDir, { skipPlugins: true, ...options });
const result = await createOrModifyExpoConfigAsync(
projectDir,
{
+16 -7
View File
@@ -26,15 +26,24 @@ export const EasNonInteractiveAndJsonFlags = {
}),
};
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
};
export const EASEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
};
export const EASMultiEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>({
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
...EasEnvironmentFlagParameters,
multiple: true,
}),
};
+16 -3
View File
@@ -6,6 +6,7 @@ import chalk from 'chalk';
import { handleDeprecatedEasJsonAsync } from '.';
import { waitForBuildEndAsync } from '../../build/build';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
@@ -21,6 +22,7 @@ import {
BuildStatus,
BuildWorkflow,
DistributionType,
EnvironmentVariableEnvironment,
IosJobOverridesInput,
ProjectArchiveSourceType,
StatuspageServiceName,
@@ -46,6 +48,7 @@ interface BuildResignFlags {
sourceProfile?: string;
maybeBuildId?: string;
wait: boolean;
environment?: EnvironmentVariableEnvironment;
}
interface RawBuildResignFlags {
json: boolean;
@@ -57,6 +60,7 @@ interface RawBuildResignFlags {
'source-profile': string | undefined;
wait: boolean;
id: string | undefined;
environment: EnvironmentVariableEnvironment | undefined;
}
export default class BuildResign extends EasCommand {
@@ -140,7 +144,15 @@ export default class BuildResign extends EasCommand {
platform,
flags.targetProfile ?? 'production'
);
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile,
buildProfileName: flags.targetProfile ?? 'production',
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.env },
});
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const build = await this.ensureBuildSelectedAsync(
{
@@ -161,7 +173,7 @@ export default class BuildResign extends EasCommand {
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
env,
easJsonCliConfig,
vcsClient,
});
@@ -181,7 +193,7 @@ export default class BuildResign extends EasCommand {
projectDir,
exp,
xcodeBuildContext,
env: buildProfile.env,
env,
vcsClient,
});
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
@@ -248,6 +260,7 @@ export default class BuildResign extends EasCommand {
targetProfile: flags['target-profile'],
maybeBuildId: flags.id,
wait: flags.wait,
environment: flags.environment,
};
}
@@ -3,6 +3,7 @@ import { EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../../commandUtils/flags';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -74,8 +75,12 @@ export default class BuildVersionGetView extends EasCommand {
});
const results: { [key in Platform]?: string } = {};
for (const { profile, platform } of buildProfiles) {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env: profile.env,
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profile,
buildProfileName: flags.profile ?? 'production',
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: profile.env },
});
validateAppConfigForRemoteVersionSource(exp, platform);
@@ -89,6 +94,7 @@ export default class BuildVersionGetView extends EasCommand {
platform,
vcsClient,
nonInteractive: flags['non-interactive'],
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -4,6 +4,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../../../commandUtils/EasCommand';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -64,7 +65,13 @@ export default class BuildVersionSetView extends EasCommand {
flags.profile ?? undefined
);
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: profile.env });
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profile,
buildProfileName: flags.profile ?? 'production',
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: profile.env },
});
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
validateAppConfigForRemoteVersionSource(exp, platform);
@@ -78,6 +85,7 @@ export default class BuildVersionSetView extends EasCommand {
platform,
vcsClient,
nonInteractive: false,
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -5,6 +5,7 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../../build/android/version';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../../build/ios/version';
import EasCommand from '../../../commandUtils/EasCommand';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -87,8 +88,12 @@ export default class BuildVersionSyncView extends EasCommand {
projectDir,
});
for (const profileInfo of buildProfiles) {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env: profileInfo.profile.env,
const { exp, projectId, env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profileInfo.profile,
buildProfileName: profileInfo.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: profileInfo.profile.env },
});
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
@@ -103,6 +108,7 @@ export default class BuildVersionSyncView extends EasCommand {
platform: profileInfo.platform,
vcsClient,
nonInteractive: false,
env,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
+22 -6
View File
@@ -4,7 +4,9 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../commandUtils/EasCommand';
import { createGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
@@ -33,6 +35,7 @@ export default class Config extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.SessionManagment,
};
async runAsync(): Promise<void> {
@@ -40,10 +43,15 @@ export default class Config extends EasCommand {
if (flags.json) {
enableJsonOutput();
}
const { platform: maybePlatform, profile: maybeProfile } = flags;
const { getDynamicPublicProjectConfigAsync, projectDir } = await this.getContextAsync(Config, {
nonInteractive: false,
});
const {
platform: maybePlatform,
profile: maybeProfile,
'non-interactive': nonInteractive,
} = flags;
const { getDynamicPublicProjectConfigAsync, projectDir, sessionManager } =
await this.getContextAsync(Config, {
nonInteractive,
});
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
const profileName =
@@ -80,8 +88,16 @@ export default class Config extends EasCommand {
Log.log(JSON.stringify(profile, null, 2));
}
} else {
const { exp: appConfig } = await getDynamicPublicProjectConfigAsync({
env: profile.env,
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const { exp: appConfig } = await evaluateConfigWithEnvVarsAsync({
buildProfile: profile,
buildProfileName: profileName,
graphqlClient,
getProjectConfig: getDynamicPublicProjectConfigAsync,
opts: { env: profile.env },
});
if (flags.json) {
@@ -0,0 +1,354 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import {
promptVariableEnvironmentAsync,
promptVariableNameAsync,
promptVariableValueAsync,
} from '../../../utils/prompts';
import EnvironmentVariableCreate from '../create';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvironmentVariableCreate, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const variableId = 'testId';
const testAccountId = 'test-account-id';
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(EnvironmentVariableMutation.createForAppAsync)
.mockImplementation(async (_client, input, _appId) => ({
...input,
id: variableId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
}));
jest
.mocked(EnvironmentVariableMutation.createSharedVariableAsync)
.mockImplementation(async (_client, input, _appId) => ({
...input,
id: variableId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
}));
jest
.mocked(EnvironmentVariableMutation.updateAsync)
.mockImplementation(async (_client, input) => ({
...input,
id: variableId,
name: 'VarName',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
}));
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockImplementation(async () => []);
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockImplementation(async () => []);
});
describe('in interactive mode', () => {
it('creates a project variable', async () => {
const command = new EnvironmentVariableCreate(
['--name', 'VarName', '--value', 'VarValue', '--environment', 'production'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testProjectId
);
});
it('updates an existing variable in the same environment', async () => {
const command = new EnvironmentVariableCreate(
['--name', 'VarName', '--value', 'VarValue', '--environment', 'production'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
const otherVariableId = 'otherId';
jest
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Project,
},
]);
// @ts-expect-error
jest.spyOn(command, 'promptForOverwriteAsync').mockResolvedValue(true);
jest
.mocked(EnvironmentVariableMutation.updateAsync)
// @ts-expect-error
.mockImplementation(async (_client, input) => ({
...input,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
}));
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
filterNames: ['VarName'],
});
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
});
it('creates a shared variable', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--scope',
'shared',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
'test-account-id'
);
});
it('throws if a shared variable already exists', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--scope',
'shared',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest
.mocked(EnvironmentVariablesQuery.sharedAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: 'otherId',
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
await expect(command.runAsync()).rejects.toThrow();
});
it('updates if a shared variable already exists and --force flag is set', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--force',
'--scope',
'shared',
],
mockConfig
);
const otherVariableId = 'otherId';
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest
.mocked(EnvironmentVariablesQuery.sharedAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
await command.runAsync();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
});
it('creates a shared variable and links it', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--environment',
'development',
'--scope',
'shared',
'--link',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [
EnvironmentVariableEnvironment.Production,
EnvironmentVariableEnvironment.Development,
],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testAccountId
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Development
);
});
it('prompts for missing arguments', async () => {
const command = new EnvironmentVariableCreate([], mockConfig);
jest.mocked(promptVariableNameAsync).mockImplementation(async () => 'VarName');
jest.mocked(promptVariableValueAsync).mockImplementation(async () => 'VarValue');
jest
.mocked(promptVariableEnvironmentAsync)
// @ts-expect-error
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(promptVariableNameAsync).toHaveBeenCalled();
expect(promptVariableValueAsync).toHaveBeenCalled();
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testProjectId
);
});
});
});
@@ -0,0 +1,127 @@
import { Config } from '@oclif/core';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableDelete from '../delete';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../log');
describe(EnvironmentVariableDelete, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
beforeEach(() => {
jest.resetAllMocks();
});
it('deletes a variable by name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Production],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'production',
'--non-interactive',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
});
it('prompts for variable selection when name is not provided', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableDelete([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
});
it('throws an error when variable name is not found', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow('Variable "NON_EXISTENT_VARIABLE" not found.');
});
it('throws an error when multiple variables with the same name are found', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
{ id: '2', name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
'Multiple variables with name "TEST_VARIABLE" found. Please select the variable to delete interactively or run command with --variable-environment ENVIRONMENT option.'
);
});
it('cancels deletion when user does not confirm', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(false);
const command = new EnvironmentVariableDelete(['--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrowErrorMatchingSnapshot();
expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,109 @@
import { Config } from '@oclif/core';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../../utils/prompts';
import EnvironmentVariableGet from '../get';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvironmentVariableGet, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const mockVariables = [
{
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
},
];
it('retrieves environment variables successfully', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
jest.spyOn(Log, 'log').mockImplementation(() => {});
const command = new EnvironmentVariableGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: undefined,
filterNames: ['TEST_VAR_1'],
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
});
it('handles errors during retrieval', async () => {
const errorMessage =
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag";
const command = new EnvironmentVariableGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await expect(command.runAsync()).rejects.toThrow(errorMessage);
});
it('prompts for variable name and environment if the name is ambigous', async () => {
jest
.mocked(promptVariableEnvironmentAsync)
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
const command = new EnvironmentVariableGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).mockResolvedValueOnce([
...mockVariables,
{
id: 'var2',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
},
]);
await command.runAsync();
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
expect(promptVariableNameAsync).toHaveBeenCalled();
});
});
@@ -0,0 +1,277 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableLink from '../link';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableLink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Linked variable ${chalk.bold('TEST_VARIABLE')} to project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('links a shared variable to the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvironmentVariableLink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('links a shared variable to the current project to a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvironmentVariableLink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('uses --variable-environment to select the variable with ambigous name', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockImplementation(
(_client, { environment }) => {
return mockVariables.filter(v => v.environments.includes(environment));
}
);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'other-id',
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({ environments: mockVariables[0].environments });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableLink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableLink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -0,0 +1,162 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import EnvironmentVariableList from '../list';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
const mockVariables: EnvironmentVariableFragment[] = [
{
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
},
{
id: 'var2',
name: 'TEST_VAR_2',
value: 'value2',
environments: [EnvironmentVariableEnvironment.Development],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
},
];
describe(EnvironmentVariableList, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('lists project environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists project environment variables in specified environments', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--environment', 'production'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Production,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists project environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--include-sensitive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: undefined,
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists shared environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--scope', 'shared'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists shared environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.sharedWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(
['--include-sensitive', '--scope', 'shared'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
});
@@ -0,0 +1,232 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableUnlink from '../unlink';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableUnlink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Unlinked variable ${chalk.bold('TEST_VARIABLE')} from project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('unlinks a shared variable from the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('unlinks a shared variable from the current project in a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Production],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -0,0 +1,164 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableUpdate from '../update';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableUpdate, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('updates a project variable by selected name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUpdate(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--non-interactive',
'--value',
'new-value',
'--name',
'NEW_VARIABLE',
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
});
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on project @testuser/testpp.`
);
});
it('updates a shared variable by selected name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUpdate(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--non-interactive',
'--value',
'new-value',
'--name',
'NEW_VARIABLE',
'--environment',
'production',
'--scope',
'shared',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
});
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on account testuser.`
);
});
it('prompts for variable selection when selected name is not provided', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({ name: mockVariables[0].name });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUpdate([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ id: variableId })
);
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on project @testuser/testpp.`
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUpdate(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
'Variable with name NON_EXISTENT_VARIABLE does not exist on project @testuser/testpp.'
);
});
});
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnvironmentVariableDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with --variable-name VARIABLE_NAME flag to fix the issue"`;
+168 -109
View File
@@ -3,12 +3,13 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EASVariableVisibilityFlag,
} from '../../commandUtils/flags';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
@@ -26,6 +27,7 @@ import {
promptVariableNameAsync,
promptVariableValueAsync,
} from '../../utils/prompts';
import { performForEnvironmentsAsync } from '../../utils/variableUtils';
type CreateFlags = {
name?: string;
@@ -34,7 +36,7 @@ type CreateFlags = {
force?: boolean;
visibility?: EnvironmentVariableVisibility;
scope?: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment;
environment?: EnvironmentVariableEnvironment[];
'non-interactive': boolean;
};
@@ -60,7 +62,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
}),
...EASVariableVisibilityFlag,
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -73,16 +75,18 @@ export default class EnvironmentVariableCreate extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariableCreate);
let {
const validatedFlags = this.validateFlags(flags);
const {
name,
value,
scope,
'non-interactive': nonInteractive,
environment,
environment: environments,
visibility,
link,
force,
} = this.validateFlags(flags);
} = await this.promptForMissingFlagsAsync(validatedFlags);
const {
privateProjectConfig: { projectId },
@@ -96,91 +100,74 @@ export default class EnvironmentVariableCreate extends EasCommand {
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
]);
if (!name) {
name = await promptVariableNameAsync(nonInteractive);
}
let overwrite = false;
visibility = visibility ?? EnvironmentVariableVisibility.Public;
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
hidden: visibility !== EnvironmentVariableVisibility.Public,
});
}
if (scope === EnvironmentVariableScope.Project) {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
filterNames: [name],
});
const existingVariable = existingVariables.find(
variable => !environments || variable.environments?.some(env => environments?.includes(env))
);
const existingVariable = existingVariables.find(variable => variable.name === name);
if (existingVariable) {
if (existingVariable.scope === EnvironmentVariableScope.Project) {
await this.promptForOverwriteAsync({
nonInteractive,
force,
message: `Variable ${name} already exists on this project.`,
suggestion: 'Do you want to overwrite it?',
});
overwrite = true;
}
if (existingVariable.scope === EnvironmentVariableScope.Shared) {
if (!nonInteractive) {
const confirmation = await confirmAsync({
message: `Shared variable ${name} already exists on this project. Do you want to unlink it first?`,
});
await this.promptForOverwriteAsync({
nonInteractive,
force,
message: `Shared variable with ${name} name already exists on this account.`,
suggestion: 'Do you want to unlink it first?',
});
if (!confirmation) {
Log.log('Aborting');
throw new Error(`Shared variable ${name} already exists on this project.`);
}
} else if (!force) {
throw new Error(
`Shared variable ${name} already exists on this project. Use --force to overwrite it.`
);
}
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
existingVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinking shared variable ${chalk.bold(name)} on project ${chalk.bold(
projectDisplayName
)}.`
);
} else {
if (!nonInteractive) {
const confirmation = await confirmAsync({
message: `Variable ${name} already exists on this project. Do you want to overwrite it?`,
});
if (!confirmation) {
Log.log('Aborting');
throw new Error(
`Variable ${name} already exists on this project. Use --force to overwrite it.`
);
}
} else if (!force) {
throw new Error(
`Variable ${name} already exists on this project. Use --force to overwrite it.`
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
existingVariable.id,
projectId,
environment
);
}
overwrite = true;
});
}
}
const variable = await EnvironmentVariableMutation.createForAppAsync(
graphqlClient,
{
name,
value,
environment,
visibility,
overwrite,
},
projectId
);
const variable =
overwrite && existingVariable
? await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: existingVariable.id,
name,
value,
visibility,
environments,
})
: await EnvironmentVariableMutation.createForAppAsync(
graphqlClient,
{
name,
value,
environments,
visibility,
type: EnvironmentSecretType.String,
},
projectId
);
if (!variable) {
throw new Error(
`Could not create variable with name ${name} on project ${projectDisplayName}`
@@ -191,39 +178,46 @@ export default class EnvironmentVariableCreate extends EasCommand {
`Created a new variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
);
} else if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
const existingVariable = sharedVariables.find(variable => variable.name === name);
const existingVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
filterNames: [name],
});
const existingVariable = existingVariables.find(
variable => !environments || variable.environments?.some(env => environments?.includes(env))
);
if (existingVariable) {
throw new Error(
`Shared variable with ${name} name already exists on this account.\n` +
`Use a different name or delete the existing variable on website or by using eas env:delete --name ${name} --scope shared command.`
);
}
if (environment && !link) {
const confirmation = await confirmAsync({
message: `Unexpected argument: --environment can only be used with --link flag. Do you want to link the variable to the current project?`,
});
if (!confirmation) {
Log.log('Aborting');
throw new Error('Unexpected argument: --environment can only be used with --link flag.');
if (force) {
overwrite = true;
} else {
throw new Error(
`Shared variable with ${name} name already exists on this account.\n` +
`Use a different name or delete the existing variable on website or by using eas env:delete --name ${name} --scope shared command.`
);
}
}
if (!environment && link) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const variable = await EnvironmentVariableMutation.createSharedVariableAsync(
graphqlClient,
{
name,
value,
visibility,
},
ownerAccount.id
);
const variable =
overwrite && existingVariable
? await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: existingVariable.id,
name,
value,
visibility,
environments,
})
: await EnvironmentVariableMutation.createSharedVariableAsync(
graphqlClient,
{
name,
value,
visibility,
environments,
type: EnvironmentSecretType.String,
},
ownerAccount.id
);
if (!variable) {
throw new Error(
@@ -235,18 +229,22 @@ export default class EnvironmentVariableCreate extends EasCommand {
`Created a new variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
);
if (link && environment) {
if (link) {
Log.withTick(
`Linking shared variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
variable.id,
projectId,
environment
);
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
variable.id,
projectId,
environment
);
});
Log.withTick(
`Linked shared variable ${chalk.bold(name)} to project ${chalk.bold(projectDisplayName)}.`
);
@@ -254,6 +252,67 @@ export default class EnvironmentVariableCreate extends EasCommand {
}
}
private async promptForOverwriteAsync({
nonInteractive,
force,
message,
suggestion,
}: {
nonInteractive: boolean;
force: boolean;
message: string;
suggestion: string;
}): Promise<void> {
if (!nonInteractive) {
const confirmation = await confirmAsync({
message: `${message} ${suggestion}`,
});
if (!confirmation) {
Log.log('Aborting');
throw new Error(`${message}`);
}
} else if (!force) {
throw new Error(`${message} Use --force to overwrite it.`);
}
}
private async promptForMissingFlagsAsync({
name,
value,
environment,
visibility = EnvironmentVariableVisibility.Public,
'non-interactive': nonInteractive,
...rest
}: CreateFlags): Promise<Required<CreateFlags>> {
if (!name) {
name = await promptVariableNameAsync(nonInteractive);
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
hidden: visibility !== EnvironmentVariableVisibility.Public,
});
}
if (!environment) {
environment = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
}
return {
name,
value,
environment,
visibility,
link: rest.link ?? false,
force: rest.force ?? false,
scope: rest.scope ?? EnvironmentVariableScope.Project,
'non-interactive': nonInteractive,
...rest,
};
}
private validateFlags(flags: CreateFlags): CreateFlags {
if (flags.scope !== EnvironmentVariableScope.Shared && flags.link) {
throw new Error(
+54 -36
View File
@@ -1,22 +1,23 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
type DeleteFlags = {
name?: string;
environment?: EnvironmentVariableEnvironment;
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
scope?: EnvironmentVariableScope;
};
@@ -27,11 +28,14 @@ export default class EnvironmentVariableDelete extends EasCommand {
static override hidden = true;
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable to delete',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to delete',
}),
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -42,7 +46,12 @@ export default class EnvironmentVariableDelete extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariableDelete);
let { name, environment, 'non-interactive': nonInteractive, scope } = this.validateFlags(flags);
const {
'variable-name': name,
'variable-environment': environment,
'non-interactive': nonInteractive,
scope,
} = this.validateFlags(flags);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
@@ -50,44 +59,53 @@ export default class EnvironmentVariableDelete extends EasCommand {
nonInteractive,
});
if (scope === EnvironmentVariableScope.Project) {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
}
const variables =
scope === EnvironmentVariableScope.Project && environment
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, projectId, environment)
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
scope === EnvironmentVariableScope.Project
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
})
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment,
});
let selectedVariable;
if (!name) {
({ name } = await promptAsync({
({ variable: selectedVariable } = await promptAsync({
type: 'select',
name: 'name',
name: 'variable',
message: 'Pick the variable to be deleted:',
choices: variables
.filter(({ scope: variableScope }) => scope === variableScope)
.map(variable => ({
title: variable.name,
value: variable.name,
})),
.map(variable => {
return {
title: formatVariableName(variable),
value: variable,
};
}),
}));
} else {
const selectedVariables = variables.filter(
variable =>
variable.name === name && (!environment || variable.environments?.includes(environment))
);
if (!name) {
throw new Error(
`Environment variable wasn't selected. Run the command again and select existing variable or run it with ${chalk.bold(
'--name VARIABLE_NAME'
)} flag to fix the issue.`
);
if (selectedVariables.length !== 1) {
if (selectedVariables.length === 0) {
throw new Error(`Variable "${name}" not found.`);
} else {
throw new Error(
`Multiple variables with name "${name}" found. Please select the variable to delete interactively or run command with --variable-environment ENVIRONMENT option.`
);
}
}
selectedVariable = selectedVariables[0];
}
const selectedVariable = variables.find(variable => variable.name === name);
if (!selectedVariable) {
throw new Error(`Variable "${name}" not found.`);
}
assert(selectedVariable, `Variable "${name}" not found.`);
if (!nonInteractive) {
Log.addNewLineIfNone();
@@ -103,7 +121,7 @@ export default class EnvironmentVariableDelete extends EasCommand {
});
if (!confirmed) {
Log.error(`Canceled deletion of variable ${selectedVariable.name}.`);
throw new Error(`Variable "${name}" not deleted.`);
throw new Error(`Variable "${selectedVariable.name}" not deleted.`);
}
}
@@ -114,10 +132,10 @@ export default class EnvironmentVariableDelete extends EasCommand {
private validateFlags(flags: DeleteFlags): DeleteFlags {
if (flags['non-interactive']) {
if (!flags.name) {
if (!flags['variable-name']) {
throw new Error(
`Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with ${chalk.bold(
'--name VARIABLE_NAME'
'--variable-name VARIABLE_NAME'
)} flag to fix the issue`
);
}
+186
View File
@@ -0,0 +1,186 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
type ParsedFlags =
| {
nonInteractive: true;
environment: EnvironmentVariableEnvironment;
}
| {
nonInteractive: false;
environment?: EnvironmentVariableEnvironment;
};
interface RawFlags {
environment?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
}
export default class EnvExec extends EasCommand {
static override description =
'execute a bash command with environment variables from the selected environment';
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASEnvironmentFlag,
...EASNonInteractiveFlag,
};
static override args = [
{
name: 'BASH_COMMAND',
required: true,
description: 'bash command to execute with the environment variables from the environment',
},
];
async runAsync(): Promise<void> {
const {
flags,
args: { BASH_COMMAND: command },
} = await this.parse(EnvExec);
const parsedFlags = this.sanitizeFlags(flags);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvExec, {
nonInteractive: parsedFlags.nonInteractive,
});
const environment =
parsedFlags.environment ??
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
const environmentVariables = await this.loadEnvironmentVariablesAsync({
graphqlClient,
projectId,
environment,
});
await this.runCommandWithEnvVarsAsync({ command, environmentVariables });
}
private sanitizeFlags(rawFlags: RawFlags): ParsedFlags {
const environment = rawFlags.environment;
if (rawFlags['non-interactive']) {
if (!environment) {
throw new Error(
'You must specify an environment when running in non-interactive mode. Use the --environment flag.'
);
}
return {
nonInteractive: true,
environment,
};
}
return {
nonInteractive: false,
environment: rawFlags.environment,
};
}
private async runCommandWithEnvVarsAsync({
command,
environmentVariables,
}: {
command: string;
environmentVariables: Record<string, string>;
}): Promise<void> {
Log.log(`Running command: ${chalk.bold(command)}`);
const spawnPromise = spawnAsync('bash', ['-c', command], {
stdio: ['inherit', 'pipe', 'pipe'],
env: {
...process.env,
...environmentVariables,
},
});
const {
child: { stdout, stderr },
} = spawnPromise;
if (!stdout || !stderr) {
throw new Error(`Failed to spawn ${command}`);
}
stdout.on('data', data => {
for (const line of data.toString().trim().split('\n')) {
Log.log(`${chalk.gray('[stdout]')} ${line}`);
}
});
stderr.on('data', data => {
for (const line of data.toString().trim().split('\n')) {
Log.warn(`${chalk.gray('[stderr]')} ${line}`);
}
});
try {
await spawnPromise;
} catch (error) {
Log.error(`❌ ${chalk.bold(command)} failed`);
throw error;
}
}
private async loadEnvironmentVariablesAsync({
graphqlClient,
projectId,
environment,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
environment: EnvironmentVariableEnvironment;
}): Promise<Record<string, string>> {
const environmentVariablesQueryResult =
await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
});
const secretEnvironmentVariables = environmentVariablesQueryResult.filter(
({ value }) => !value
);
if (secretEnvironmentVariables.length > 0) {
Log.warn(`The following environment variables are secret and cannot be downloaded locally:`);
for (const { name } of secretEnvironmentVariables) {
Log.warn(`- ${name}`);
}
Log.warn('Proceeding with the rest of the environment variables.');
Log.newLine();
}
const nonSecretEnvironmentVariables = environmentVariablesQueryResult.filter(
({ value }) => !!value
);
if (nonSecretEnvironmentVariables.length === 0) {
throw new Error('No readable environment variables found for the selected environment.');
}
Log.log(
`Loaded environment variables for the selected environment "${environment.toLowerCase()}":`
);
for (const { name } of nonSecretEnvironmentVariables) {
Log.log(`- ${name}`);
}
Log.newLine();
const environmentVariables: Record<string, string> = {};
for (const { name, value } of nonSecretEnvironmentVariables) {
if (value) {
environmentVariables[name] = value;
}
}
return environmentVariables;
}
}
+69 -42
View File
@@ -1,13 +1,14 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import {
EnvironmentVariableEnvironment,
@@ -16,12 +17,12 @@ import {
} from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { formatVariable } from '../../utils/formatVariable';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
import { formatVariable } from '../../utils/variableUtils';
type GetFlags = {
name?: string;
environment?: EnvironmentVariableEnvironment;
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
format?: string;
scope: EnvironmentVariableScope;
@@ -38,21 +39,24 @@ export default class EnvironmentVariableGet extends EasCommand {
};
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable',
}),
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASNonInteractiveFlag,
...EASEnvironmentFlag,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariableGet);
let {
environment,
name,
'variable-environment': environment,
'variable-name': name,
'non-interactive': nonInteractive,
format,
scope,
@@ -69,16 +73,41 @@ export default class EnvironmentVariableGet extends EasCommand {
name = await promptVariableNameAsync(nonInteractive);
}
if (!environment && scope === EnvironmentVariableScope.Project) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const variables = await getVariablesAsync(graphqlClient, scope, projectId, name, environment);
const variable = await getVariableAsync(graphqlClient, scope, projectId, name, environment);
if (!variable) {
if (variables.length === 0) {
Log.error(`Variable with name "${name}" not found`);
return;
}
let variable;
if (variables.length > 1) {
if (!environment) {
const availableEnvironments = variables.reduce<EnvironmentVariableEnvironment[]>(
(acc, v) => [...acc, ...(v.environments ?? [])],
[] as EnvironmentVariableEnvironment[]
);
environment = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: false,
availableEnvironments,
});
}
assert(environment, 'Environment is required.');
const variableInEnvironment = variables.find(v => v.environments?.includes(environment!));
if (!variableInEnvironment) {
throw new Error(`Variable with name "${name}" not found in environment "${environment}"`);
}
variable = variableInEnvironment;
} else {
variable = variables[0];
}
if (!variable.value) {
throw new Error(
`${chalk.bold(
@@ -95,17 +124,14 @@ export default class EnvironmentVariableGet extends EasCommand {
}
private validateFlags(flags: GetFlags): GetFlags {
if (flags.environment && flags.scope === EnvironmentVariableScope.Shared) {
throw new Error(`Unexpected argument: --environment can only be used with project variables`);
}
if (flags['non-interactive']) {
if (!flags.name) {
throw new Error('Variable name is required. Run the command with --name flag.');
if (!flags['variable-name']) {
throw new Error('Variable name is required. Run the command with --variable-name flag.');
}
if (!flags.scope) {
throw new Error('Scope is required. Run the command with --scope flag.');
}
if (!flags.environment && flags.scope === EnvironmentVariableScope.Project) {
if (!flags['variable-environment']) {
throw new Error('Environment is required.');
}
}
@@ -113,32 +139,33 @@ export default class EnvironmentVariableGet extends EasCommand {
}
}
async function getVariableAsync(
async function getVariablesAsync(
graphqlClient: ExpoGraphqlClient,
scope: string,
projectId: string,
name: string,
environment: string | undefined
): Promise<EnvironmentVariableFragment | null> {
if (!environment && scope === EnvironmentVariableScope.Project) {
throw new Error('Environment is required.');
}
if (environment && scope === EnvironmentVariableScope.Project) {
const appVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment,
[name]
name: string | undefined,
environment: EnvironmentVariableEnvironment | undefined
): Promise<EnvironmentVariableFragment[]> {
if (!name) {
throw new Error(
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag."
);
return appVariables[0];
}
if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId, [
name,
]);
return sharedVariables[0];
if (scope === EnvironmentVariableScope.Project) {
const appVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
filterNames: [name],
});
return appVariables;
} else {
const sharedVariables = await EnvironmentVariablesQuery.sharedWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
filterNames: [name],
}
);
return sharedVariables;
}
return null;
}
+97 -27
View File
@@ -2,13 +2,19 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import {
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
export default class EnvironmentVariableLink extends EasCommand {
static override description = 'link a shared environment variable to the current project';
@@ -16,10 +22,14 @@ export default class EnvironmentVariableLink extends EasCommand {
static override hidden = true;
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable',
}),
...EASEnvironmentFlag,
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to link',
}),
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -30,7 +40,12 @@ export default class EnvironmentVariableLink extends EasCommand {
async runAsync(): Promise<void> {
let {
flags: { name, 'non-interactive': nonInteractive, environment },
flags: {
'variable-name': name,
'variable-environment': currentEnvironment,
'non-interactive': nonInteractive,
environment: environments,
},
} = await this.parse(EnvironmentVariableLink);
const {
privateProjectConfig: { projectId },
@@ -40,44 +55,99 @@ export default class EnvironmentVariableLink extends EasCommand {
});
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment: currentEnvironment,
filterNames: name ? [name] : undefined,
});
if (!name) {
name = await selectAsync(
let selectedVariable = variables[0];
if (variables.length > 1) {
selectedVariable = await selectAsync(
'Select shared variable',
variables.map(variable => ({
title: variable.name,
value: variable.name,
title: formatVariableName(variable),
value: variable,
}))
);
}
const selectedVariable = variables.find(variable => variable.name === name);
if (!selectedVariable) {
throw new Error(`Shared variable ${name} doesn't exist`);
}
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
let explicitSelect = false;
if (!nonInteractive && !environments) {
const selectedEnvironments =
(selectedVariable.linkedEnvironments ?? []).length > 0
? selectedVariable.linkedEnvironments
: selectedVariable.environments;
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedEnvironments ?? [],
});
explicitSelect = true;
}
const linkedVariable = await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
if (!linkedVariable) {
throw new Error(
`Could not link variable with name ${selectedVariable.name} to project with id ${projectId}`
if (!environments) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
);
return;
}
Log.withTick(
`Linked variable ${chalk.bold(linkedVariable.name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
try {
if (
selectedVariable.linkedEnvironments?.includes(environment) ===
environments.includes(environment)
) {
if (!explicitSelect && environments.includes(environment)) {
Log.withTick(
`Variable ${chalk.bold(
selectedVariable.name
)} is already linked to ${environment.toLocaleLowerCase()}.`
);
}
continue;
}
if (environments.includes(environment)) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
} else if (explicitSelect) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
}
} catch (err: any) {
Log.warn(err.message);
}
}
}
}
+88 -28
View File
@@ -1,16 +1,57 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentFlag,
EASMultiEnvironmentFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
} from '../../commandUtils/flags';
import { EnvironmentVariableScope } from '../../graphql/generated';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
} from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { formatVariable } from '../../utils/formatVariable';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariable, performForEnvironmentsAsync } from '../../utils/variableUtils';
async function getVariablesForScopeAsync(
graphqlClient: ExpoGraphqlClient,
{
scope,
includingSensitive,
environment,
projectId,
}: {
scope: EnvironmentVariableScope;
includingSensitive: boolean;
environment?: EnvironmentVariableEnvironment;
projectId: string;
}
): Promise<EnvironmentVariableFragment[]> {
if (scope === EnvironmentVariableScope.Project) {
if (includingSensitive) {
return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return includingSensitive
? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
})
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId, environment });
}
export default class EnvironmentValueList extends EasCommand {
static override description = 'list environment variables for the current project';
@@ -23,14 +64,24 @@ export default class EnvironmentValueList extends EasCommand {
};
static override flags = {
'include-sensitive': Flags.boolean({
description: 'Display sensitive values in the output',
default: false,
}),
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
};
async runAsync(): Promise<void> {
let {
flags: { environment, format, scope },
flags: {
environment: environments,
format,
scope,
'include-sensitive': includeSensitive,
'non-interactive': nonInteractive,
},
} = await this.parse(EnvironmentValueList);
const {
privateProjectConfig: { projectId },
@@ -39,34 +90,43 @@ export default class EnvironmentValueList extends EasCommand {
nonInteractive: true,
});
if (scope === EnvironmentVariableScope.Project && !environment) {
environment = await promptVariableEnvironmentAsync(false);
if (!environments) {
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
}
const variables =
scope === EnvironmentVariableScope.Project && environment
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, projectId, environment)
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
await performForEnvironmentsAsync(environments, async environment => {
const variables = await getVariablesForScopeAsync(graphqlClient, {
scope,
includingSensitive: includeSensitive,
environment,
projectId,
});
if (format === 'short') {
for (const variable of variables) {
// TODO: Add Learn more link
Log.addNewLineIfNone();
if (environment) {
Log.log(chalk.bold(`Environment: ${environment}`));
}
if (format === 'short') {
for (const variable of variables) {
// TODO: Add Learn more link
Log.log(
`${chalk.bold(variable.name)}=${
variable.value ??
"***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)"
}`
);
}
} else {
if (scope === EnvironmentVariableScope.Shared) {
Log.log(chalk.bold('Shared variables for this account:'));
} else {
Log.log(chalk.bold(`Variables for this project:`));
}
Log.log(
`${chalk.bold(variable.name)}=${
variable.value ??
"***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)"
}`
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
);
}
} else {
if (scope === EnvironmentVariableScope.Shared) {
Log.log(chalk.bold('Shared variables for this account:'));
} else {
Log.log(chalk.bold(`Variables for this project for environment ${environment}:`));
}
Log.log(
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
);
}
});
}
}
+94
View File
@@ -0,0 +1,94 @@
import { Flags } from '@oclif/core';
import * as fs from 'fs-extra';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableFragment } from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
export default class EnvironmentVariablePull extends EasCommand {
static override description = 'pull env file';
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASEnvironmentFlag,
...EASNonInteractiveFlag,
path: Flags.string({
description: 'Path to the result `.env` file',
default: '.env.local',
}),
};
async runAsync(): Promise<void> {
let {
flags: { environment, path: targetPath, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentVariablePull);
if (!environment) {
environment = await promptVariableEnvironmentAsync({ nonInteractive });
}
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariablePull, {
nonInteractive,
});
targetPath = targetPath ?? '.env.local';
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
environment,
}
);
if (!nonInteractive && (await fs.exists(targetPath))) {
const result = await confirmAsync({
message: `File ${targetPath} already exists. Do you want to overwrite it?`,
});
if (!result) {
Log.log('Aborting...');
throw new Error(`File ${targetPath} already exists.`);
}
}
const filePrefix = `# Environment: ${environment}\n\n`;
const envFileContent = environmentVariables
.map((variable: EnvironmentVariableFragment) => {
if (variable.value === null) {
return `# ${variable.name}=***** (secret variables are not available for reading)`;
}
return `${variable.name}=${variable.value}`;
})
.join('\n');
await fs.writeFile(targetPath, filePrefix + envFileContent);
const secretEnvVariables = environmentVariables.filter(
(variable: EnvironmentVariableFragment) => variable.value === null
);
if (secretEnvVariables.length > 0) {
Log.warn(
`The eas env:pull command tried to pull environment variables with "secret" visibility. The variables with "secret" visibility are not available for reading, therefore thet were marked as "*****" in the generated .env file. Provide values for these manually in ${targetPath} if needed. Skipped variables: ${secretEnvVariables
.map(v => v.name)
.join('\n')}`
);
Log.warn();
}
Log.log(
`Pulled environment variables from ${environment.toLowerCase()} environment to ${targetPath}.`
);
}
}
+210
View File
@@ -0,0 +1,210 @@
import { Flags } from '@oclif/core';
import dotenv from 'dotenv';
import fs from 'fs-extra';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag } from '../../commandUtils/flags';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../graphql/generated';
import {
EnvironmentVariableMutation,
EnvironmentVariablePushInput,
} from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
type PushFlags = {
environment?: EnvironmentVariableEnvironment;
path: string;
};
export default class EnvironmentVariablePush extends EasCommand {
static override description = 'push env file';
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASEnvironmentFlag,
path: Flags.string({
description: 'Path to the input `.env` file',
default: '.env.local',
}),
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariablePush);
const { environment, path: envPath } = this.validateFlags(flags);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariablePush, {
nonInteractive: false,
});
const updateVariables: Record<string, EnvironmentVariablePushInput> =
await this.parseEnvFileAsync(envPath, environment);
const variableNames = Object.keys(updateVariables);
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
filterNames: variableNames,
});
const existingDifferentVariables: EnvironmentVariableFragment[] = [];
// Remove variables that are the same as the ones in the environment
existingVariables.forEach(variable => {
const existingVariableUpdate = updateVariables[variable.name];
if (existingVariableUpdate && existingVariableUpdate.value !== variable.value) {
existingDifferentVariables.push(variable);
} else {
delete updateVariables[variable.name];
}
});
const existingDifferentSharedVariables = existingDifferentVariables.filter(
variable => variable.scope === EnvironmentVariableScope.Shared
);
if (existingDifferentSharedVariables.length > 0) {
const existingDifferentSharedVariablesNames = existingDifferentSharedVariables.map(
variable => variable.name
);
Log.error('Shared variables cannot be overwritten by eas env:push command.');
Log.error('Remove them from the env file or unlink them from the project to continue:');
existingDifferentSharedVariablesNames.forEach(name => {
Log.error(`- ${name}`);
});
throw new Error('Shared variables cannot be overwritten by eas env:push command');
}
if (existingDifferentVariables.length > 0) {
Log.warn('Some variables already exist in the environment.');
const variableNames = existingDifferentVariables.map(variable => variable.name);
const confirmationMessage =
variableNames.length > 1
? `The ${variableNames.join(
', '
)} environment variables already exist in ${environment} environment. Do you want to override them all?`
: `The ${variableNames[0]} environment variable already exists in ${environment} environment. Do you want to override it?`;
const confirm = await confirmAsync({
message: confirmationMessage,
});
let variablesToOverwrite: string[] = [];
if (!confirm && existingDifferentVariables.length === 1) {
throw new Error('No new variables to push.');
}
if (confirm) {
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
} else {
const promptResult = await promptAsync({
type: 'multiselect',
name: 'variablesToOverwrite',
message: 'Select variables to overwrite:',
// @ts-expect-error property missing from `@types/prompts`
optionsPerPage: 20,
choices: existingDifferentVariables.map(variable => ({
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
variable.value ?? '(secret)'
})`,
value: variable.name,
})),
});
variablesToOverwrite = promptResult.variablesToOverwrite;
}
for (const existingVariable of existingVariables) {
const name = existingVariable.name;
if (variablesToOverwrite.includes(name)) {
updateVariables[name]['overwrite'] = true;
} else {
delete updateVariables[name];
}
}
}
// Check if any of the sensitive variables already exist in the environment. Prompt the user to overwrite them.
const existingSensitiveVariables = existingVariables.filter(
variable => variable.value === null
);
if (existingSensitiveVariables.length > 0) {
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
variable => `- ${variable.name}`
);
const confirm = await confirmAsync({
message: `You are about to overwrite sensitive variables.\n${existingSensitiveVariablesNames.join(
'\n'
)}\n Do you want to continue?`,
});
if (!confirm) {
throw new Error('Aborting...');
}
}
const variablesToPush = Object.values(updateVariables);
if (variablesToPush.length === 0) {
Log.log('No new variables to push.');
return;
}
await EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync(
graphqlClient,
variablesToPush,
projectId
);
Log.log(`Uploaded env file to ${environment} environment.`);
}
private async parseEnvFileAsync(
envPath: string,
environment: string
): Promise<Record<string, EnvironmentVariablePushInput>> {
if (!(await fs.exists(envPath))) {
throw new Error(`File ${envPath} does not exist.`);
}
const pushInput: Record<string, EnvironmentVariablePushInput> = {};
const variables: Record<string, string> = dotenv.parse(await fs.readFile(envPath, 'utf8'));
for (const [name, value] of Object.entries(variables)) {
pushInput[name] = {
name,
value,
environment,
visibility: name.startsWith('EXPO_SENSITIVE')
? EnvironmentVariableVisibility.Sensitive
: EnvironmentVariableVisibility.Public,
};
}
return pushInput;
}
private validateFlags(flags: PushFlags): Required<PushFlags> {
if (!flags.environment) {
throw new Error('Please provide an environment to push the env file to.');
}
return { ...flags, environment: flags.environment };
}
}
+92 -43
View File
@@ -2,14 +2,15 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableScope } from '../../graphql/generated';
import { EASMultiEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
export default class EnvironmentVariableUnlink extends EasCommand {
static override description = 'unlink a shared environment variable to the current project';
@@ -17,10 +18,10 @@ export default class EnvironmentVariableUnlink extends EasCommand {
static override hidden = true;
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable',
}),
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -31,7 +32,11 @@ export default class EnvironmentVariableUnlink extends EasCommand {
async runAsync(): Promise<void> {
let {
flags: { name, 'non-interactive': nonInteractive, environment },
flags: {
'variable-name': name,
'non-interactive': nonInteractive,
environment: unlinkEnvironments,
},
} = await this.parse(EnvironmentVariableUnlink);
const {
privateProjectConfig: { projectId },
@@ -40,58 +45,102 @@ export default class EnvironmentVariableUnlink extends EasCommand {
nonInteractive,
});
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
filterNames: name ? [name] : undefined,
});
const appVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment
);
const linkedVariables = appVariables.filter(
({ scope }) => scope === EnvironmentVariableScope.Shared
);
let selectedVariable = variables[0];
if (linkedVariables.length === 0) {
throw new Error(`There are no linked shared env variables for project ${projectDisplayName}`);
}
if (!name) {
name = await selectAsync(
'Select shared variable to unlink',
linkedVariables.map(variable => ({
title: variable.name,
value: variable.name,
if (variables.length > 1) {
if (nonInteractive) {
throw new Error("Multiple variables found, please select one using '--variable-name'");
}
selectedVariable = await selectAsync(
'Select shared variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
}
const selectedVariable = linkedVariables.find(variable => variable.name === name);
if (!selectedVariable) {
throw new Error(`Shared variable ${name} doesn't exist`);
}
const unlinkedVariable = await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
let explicitSelect = false;
if (!unlinkedVariable) {
throw new Error(
`Could not unlink variable with name ${selectedVariable.name} from project ${projectDisplayName}`
if (!nonInteractive && !unlinkEnvironments) {
const selectedEnvironments =
(selectedVariable.linkedEnvironments ?? []).length > 0
? selectedVariable.linkedEnvironments
: selectedVariable.environments;
const environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedEnvironments ?? [],
});
explicitSelect = true;
unlinkEnvironments = Object.values(EnvironmentVariableEnvironment).filter(
env => !environments.includes(env)
);
}
Log.withTick(
`Unlinked variable ${chalk.bold(unlinkedVariable.name)} from the project ${chalk.bold(
projectDisplayName
)}.`
);
if (!unlinkEnvironments) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
);
return;
}
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
if (
selectedVariable.linkedEnvironments?.includes(environment) !==
unlinkEnvironments.includes(environment)
) {
if (!explicitSelect && unlinkEnvironments.includes(environment)) {
Log.withTick(
`Variable ${chalk.bold(
selectedVariable.name
)} is already unlinked from ${environment.toLocaleLowerCase()}.`
);
}
continue;
}
if (unlinkEnvironments.includes(environment)) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
} else if (explicitSelect) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
}
}
}
}
+121 -123
View File
@@ -1,14 +1,21 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EASVariableVisibilityFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableScope, EnvironmentVariableVisibility } from '../../graphql/generated';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
@@ -17,14 +24,22 @@ import {
getOwnerAccountForProjectIdAsync,
} from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync, promptVariableValueAsync } from '../../utils/prompts';
import {
promptVariableEnvironmentAsync,
promptVariableNameAsync,
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
type UpdateFlags = {
name?: string;
value?: string;
scope?: EnvironmentVariableScope;
environment?: string;
environment?: EnvironmentVariableEnvironment[];
visibility?: EnvironmentVariableVisibility;
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
};
@@ -35,15 +50,22 @@ export default class EnvironmentVariableUpdate extends EasCommand {
static override hidden = true;
static override flags = {
'variable-name': Flags.string({
description: 'Current name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to update',
}),
name: Flags.string({
description: 'Name of the variable',
description: 'New name of the variable',
}),
value: Flags.string({
description: 'Text value or the variable',
description: 'New value or the variable',
}),
...EASVariableVisibilityFlag,
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -59,8 +81,10 @@ export default class EnvironmentVariableUpdate extends EasCommand {
name,
value,
scope,
'variable-name': currentName,
'variable-environment': currentEnvironment,
'non-interactive': nonInteractive,
environment,
environment: environments,
visibility,
} = this.validateFlags(flags);
@@ -76,127 +100,101 @@ export default class EnvironmentVariableUpdate extends EasCommand {
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
]);
let selectedVariable: EnvironmentVariableFragment;
let existingVariables: EnvironmentVariableFragment[] = [];
const suffix =
scope === EnvironmentVariableScope.Project
? `on project ${projectDisplayName}`
: `on account ${ownerAccount.name}`;
if (scope === EnvironmentVariableScope.Project) {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(
graphqlClient,
projectId,
environment
);
if (!name) {
name = await selectAsync(
'Select variable',
existingVariables.map(variable => ({
title: variable.name,
value: variable.name,
}))
);
}
const existingVariable = existingVariables.find(variable => variable.name === name);
if (!existingVariable) {
throw new Error(
`Variable with name ${name} does not exist on project ${projectDisplayName}`
);
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
required: false,
initial: existingVariable.value,
});
if (!value || value.length === 0) {
value = '';
}
}
const variable = await EnvironmentVariableMutation.createForAppAsync(
graphqlClient,
{
name,
value,
environment,
visibility,
overwrite: true,
},
projectId
);
if (!variable) {
throw new Error(
`Could not update variable with name ${name} on project ${projectDisplayName}`
);
}
Log.withTick(
`Updated variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
);
} else if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, projectId);
if (!name) {
name = await selectAsync(
'Select variable',
sharedVariables.map(variable => ({
title: variable.name,
value: variable.name,
}))
);
}
const existingVariable = sharedVariables.find(variable => variable.name === name);
if (!existingVariable) {
throw new Error(
"Variable with this name doesn't exist on this account. Use a different name."
);
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
required: false,
initial: existingVariable.value,
});
if (!value || value.length === 0) {
value = '';
}
}
const variable = await EnvironmentVariableMutation.createSharedVariableAsync(
graphqlClient,
{
name,
value,
visibility,
overwrite: true,
},
ownerAccount.id
);
if (!variable) {
throw new Error(
`Could not update variable with name ${name} on account ${ownerAccount.name}`
);
}
Log.withTick(
`Updated shared variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
);
existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment: currentEnvironment,
filterNames: currentName ? [currentName] : undefined,
});
}
if (scope === EnvironmentVariableScope.Shared) {
existingVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment: currentEnvironment,
filterNames: currentName ? [currentName] : undefined,
});
}
if (existingVariables.length === 0) {
throw new Error(
`Variable with name ${currentName} ${
currentEnvironment ? `in environment ${currentEnvironment}` : ''
} does not exist ${suffix}.`
);
} else if (existingVariables.length > 1) {
selectedVariable = await selectAsync(
'Select variable',
existingVariables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
} else {
selectedVariable = existingVariables[0];
}
assert(selectedVariable, 'Variable must be selected');
if (!nonInteractive) {
if (!name) {
name = await promptVariableNameAsync(nonInteractive, selectedVariable.name);
if (!name || name.length === 0) {
name = undefined;
}
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
required: false,
initial: selectedVariable.value,
});
if (!value || value.length === 0) {
value = undefined;
}
}
if (!environments || environments.length === 0) {
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedVariable.environments ?? [],
});
}
if (!visibility) {
visibility = await promptVariableVisibilityAsync(
nonInteractive,
selectedVariable.visibility
);
}
}
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: selectedVariable.id,
name,
value,
environments,
visibility,
});
if (!variable) {
throw new Error(`Could not update variable with name ${name} ${suffix}`);
}
Log.withTick(`Updated variable ${chalk.bold(selectedVariable.name)} ${suffix}.`);
}
private validateFlags(flags: UpdateFlags): UpdateFlags {
if (flags['non-interactive']) {
if (!flags.name) {
if (!flags['variable-name']) {
throw new Error(
'Variable name is required in non-interactive mode. Run the command with --name flag.'
);
}
if (flags.scope === EnvironmentVariableScope.Project && !flags.environment) {
throw new Error(
'Environment is required when updating project-wide variable in non-interactive mode. Run the command with --environment flag.'
'Current name is required in non-interactive mode. Run the command with --variable-name flag.'
);
}
}
+54 -23
View File
@@ -8,6 +8,7 @@ import { getProjectDashboardUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { AppPrivacy, Role } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
@@ -33,14 +34,10 @@ export default class ProjectInit extends EasCommand {
description: 'ID of the EAS project to link',
}),
force: Flags.boolean({
description: 'Whether to overwrite any existing project ID',
dependsOn: ['id'],
}),
// this is the same as EASNonInteractiveFlag but with the dependsOn
'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.',
dependsOn: ['id'],
description:
'Whether to create a new project/link an existing project without additional prompts or overwrite any existing project ID when running with --id flag',
}),
...EASNonInteractiveFlag,
};
static override contextDefinition = {
@@ -216,10 +213,11 @@ export default class ProjectInit extends EasCommand {
});
}
private static async initializeWithInteractiveSelectionAsync(
private static async initializeWithoutExplicitIDAsync(
graphqlClient: ExpoGraphqlClient,
actor: Actor,
projectDir: string
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
): Promise<string> {
const exp = getPrivateExpoConfig(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;
@@ -245,6 +243,20 @@ export default class ProjectInit extends EasCommand {
if (!accountName) {
if (allAccounts.length === 1) {
accountName = allAccounts[0].name;
} else if (nonInteractive) {
if (!force) {
throw new Error(
`There are multiple accounts that you have access to: ${allAccounts
.map(a => a.name)
.join(
', '
)}. Explicitly set the owner property in your app config or run this command with the --force flag to proceed with a default account: ${
allAccounts[0].name
}.`
);
}
accountName = allAccounts[0].name;
Log.log(`Using default account ${accountName} for non-interactive and force mode`);
} else {
const choices = ProjectInit.getAccountChoices(
actor,
@@ -274,13 +286,21 @@ export default class ProjectInit extends EasCommand {
projectName
);
if (existingProjectIdOnServer) {
const affirmedLink = await confirmAsync({
message: `Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Link this project?`,
});
if (!affirmedLink) {
throw new Error(
`Project ID configuration canceled. Re-run the command to select a different account/project.`
);
if (!force) {
if (nonInteractive) {
throw new Error(
`Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Use --force flag to continue with this project.`
);
}
const affirmedLink = await confirmAsync({
message: `Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Link this project?`,
});
if (!affirmedLink) {
throw new Error(
`Project ID configuration canceled. Re-run the command to select a different account/project.`
);
}
}
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, existingProjectIdOnServer);
@@ -293,11 +313,18 @@ export default class ProjectInit extends EasCommand {
);
}
const affirmedCreate = await confirmAsync({
message: `Would you like to create a project for ${projectFullName}?`,
});
if (!affirmedCreate) {
throw new Error(`Project ID configuration canceled for ${projectFullName}.`);
if (!force) {
if (nonInteractive) {
throw new Error(
`Project does not exist: ${projectFullName}. Use --force flag to create this project.`
);
}
const affirmedCreate = await confirmAsync({
message: `Would you like to create a project for ${projectFullName}?`,
});
if (!affirmedCreate) {
throw new Error(`Project ID configuration canceled for ${projectFullName}.`);
}
}
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
@@ -405,10 +432,14 @@ export default class ProjectInit extends EasCommand {
});
idForConsistency = idArgument;
} else {
idForConsistency = await ProjectInit.initializeWithInteractiveSelectionAsync(
idForConsistency = await ProjectInit.initializeWithoutExplicitIDAsync(
graphqlClient,
actor,
projectDir
projectDir,
{
force,
nonInteractive,
}
);
}
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config-types';
import { Platform } from '@expo/eas-build-job';
import { EasJson } from '@expo/eas-json';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
@@ -411,6 +411,7 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({
const easJson: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
appVersionSource: AppVersionSource.REMOTE,
},
build: {
development: {
@@ -431,6 +432,7 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({
},
production: {
channel: 'production',
autoIncrement: true,
...easBuildGitHubConfig,
},
},
@@ -0,0 +1,217 @@
import { Platform, SubmissionConfig } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import nullthrows from 'nullthrows';
import { z } from 'zod';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
AndroidSubmissionConfigInput,
IosSubmissionConfigInput,
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
} from '../../graphql/generated';
import { AppStoreConnectApiKeyQuery } from '../../graphql/queries/AppStoreConnectApiKeyQuery';
import { GoogleServiceAccountKeyQuery } from '../../graphql/queries/GoogleServiceAccountKeyQuery';
import AndroidSubmitCommand from '../../submit/android/AndroidSubmitCommand';
import { SubmissionContext, createSubmissionContextAsync } from '../../submit/context';
import IosSubmitCommand from '../../submit/ios/IosSubmitCommand';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
/**
* This command will be run on the EAS workers.
* This command resolves credentials and other
* configuration, that normally would be included in the
* job and metadata objects, and prints them to stdout.
*/
export default class SubmitInternal extends EasCommand {
static override hidden = true;
static override flags = {
platform: Flags.enum<Platform>({
options: [Platform.ANDROID, Platform.IOS],
required: true,
}),
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
id: Flags.string({
description: 'ID of the build to submit',
required: true,
}),
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(SubmitInternal);
// This command is always run with implicit --non-interactive and --json options
enableJsonOutput();
const {
loggedIn: { actor, graphqlClient },
privateProjectConfig: { exp, projectId, projectDir },
analytics,
vcsClient,
} = await this.getContextAsync(SubmitInternal, {
nonInteractive: true,
vcsClientOverride: new GitNoCommitClient(),
});
const submissionProfile = await EasJsonUtils.getSubmitProfileAsync(
EasJsonAccessor.fromProjectPath(projectDir),
flags.platform,
flags.profile
);
const ctx = await createSubmissionContextAsync({
platform: flags.platform,
projectDir,
profile: submissionProfile,
archiveFlags: {
id: flags.id,
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
actor,
graphqlClient,
analytics,
exp,
projectId,
vcsClient,
specifiedProfile: flags.profile,
});
let config;
if (ctx.platform === Platform.IOS) {
const command = new IosSubmitCommand(ctx as SubmissionContext<Platform.IOS>);
const submitter = await command.runAsync();
const mutationInput = await submitter.getSubmissionInputAsync();
const iosConfig = mutationInput.submissionConfig;
const ascApiKeyJson = await getAppStoreConnectApiKeyJsonAsync({
iosConfig,
graphqlClient,
});
const configInput: z.input<typeof SubmissionConfig.Ios.SchemaZ> = {
ascAppIdentifier: iosConfig.ascAppIdentifier,
isVerboseFastlaneEnabled: iosConfig.isVerboseFastlaneEnabled ?? undefined,
...(ascApiKeyJson
? { ascApiJsonKey: ascApiKeyJson }
: {
appleIdUsername: nullthrows(iosConfig.appleIdUsername),
appleAppSpecificPassword: nullthrows(iosConfig.appleAppSpecificPassword),
}),
};
config = SubmissionConfig.Ios.SchemaZ.parse(configInput);
} else if (ctx.platform === Platform.ANDROID) {
const command = new AndroidSubmitCommand(ctx as SubmissionContext<Platform.ANDROID>);
const submitter = await command.runAsync();
const mutationInput = await submitter.getSubmissionInputAsync();
const androidConfig = mutationInput.submissionConfig;
const changesNotSentForReview = androidConfig.changesNotSentForReview ?? undefined;
const releaseStatus = androidConfig.releaseStatus
? graphQlReleaseStatusToConfigReleaseStatus[androidConfig.releaseStatus]
: undefined;
const googleServiceAccountKeyJson = nullthrows(
await getGoogleServiceAccountKeyJsonAsync({
androidConfig,
graphqlClient,
})
);
const track = graphQlTrackToConfigTrack[androidConfig.track];
const configInput: z.input<typeof SubmissionConfig.Android.SchemaZ> = {
changesNotSentForReview,
googleServiceAccountKeyJson,
track,
...(releaseStatus === SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS
? {
releaseStatus: SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS,
rollout: androidConfig.rollout ?? undefined,
}
: { releaseStatus }),
};
config = SubmissionConfig.Android.SchemaZ.parse(configInput);
} else {
throw new Error(`Unsupported platform: ${ctx.platform}`);
}
printJsonOnlyOutput({ config });
}
}
async function getGoogleServiceAccountKeyJsonAsync({
androidConfig,
graphqlClient,
}: {
androidConfig: AndroidSubmissionConfigInput;
graphqlClient: ExpoGraphqlClient;
}): Promise<string | null> {
if (androidConfig.googleServiceAccountKeyJson) {
return androidConfig.googleServiceAccountKeyJson;
} else if (androidConfig.googleServiceAccountKeyId) {
const key = await GoogleServiceAccountKeyQuery.getByIdAsync(
graphqlClient,
androidConfig.googleServiceAccountKeyId
);
return key.keyJson;
}
return null;
}
async function getAppStoreConnectApiKeyJsonAsync({
iosConfig,
graphqlClient,
}: {
iosConfig: IosSubmissionConfigInput;
graphqlClient: ExpoGraphqlClient;
}): Promise<string | null> {
if (iosConfig.ascApiKey) {
return JSON.stringify({
key_id: iosConfig.ascApiKey.keyIdentifier,
issuer_id: iosConfig.ascApiKey.issuerIdentifier,
key: iosConfig.ascApiKey.keyP8,
});
} else if (iosConfig.ascApiKeyId) {
const key = await AppStoreConnectApiKeyQuery.getByIdAsync(graphqlClient, iosConfig.ascApiKeyId);
return JSON.stringify({
key_id: key.keyIdentifier,
issuer_id: key.issuerIdentifier,
key: key.keyP8,
});
}
return null;
}
const graphQlReleaseStatusToConfigReleaseStatus = {
[SubmissionAndroidReleaseStatus.Draft]: SubmissionConfig.Android.ReleaseStatus.DRAFT,
[SubmissionAndroidReleaseStatus.InProgress]: SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS,
[SubmissionAndroidReleaseStatus.Completed]: SubmissionConfig.Android.ReleaseStatus.COMPLETED,
[SubmissionAndroidReleaseStatus.Halted]: SubmissionConfig.Android.ReleaseStatus.HALTED,
};
const graphQlTrackToConfigTrack = {
[SubmissionAndroidTrack.Production]: SubmissionConfig.Android.ReleaseTrack.PRODUCTION,
[SubmissionAndroidTrack.Beta]: SubmissionConfig.Android.ReleaseTrack.BETA,
[SubmissionAndroidTrack.Alpha]: SubmissionConfig.Android.ReleaseTrack.ALPHA,
[SubmissionAndroidTrack.Internal]: SubmissionConfig.Android.ReleaseTrack.INTERNAL,
};
@@ -21,7 +21,7 @@ import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { resolveVcsClient } from '../../../vcs';
const projectRoot = '/test-project';
@@ -47,7 +47,7 @@ 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('../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
@@ -106,7 +106,9 @@ describe(UpdatePublish.name, () => {
const { projectId } = mockTestProject();
const { platforms, runtimeVersion } = mockTestExport();
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: 'branch123', branchName: 'branchFromChannel' });
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
@@ -138,7 +140,16 @@ describe(UpdatePublish.name, () => {
// Add configuration to the project that should not be included in the update
const { appJson } = mockTestProject({
expoConfig: {},
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
const { platforms, runtimeVersion } = mockTestExport({ platforms: ['ios'] });
@@ -11,8 +11,9 @@ 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 { BranchQuery } from '../../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../../graphql/queries/UpdateQuery';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { selectUpdateGroupOnBranchAsync } from '../../../update/queries';
import {
getCodeSigningInfoAsync,
@@ -44,7 +45,8 @@ jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/queries/BranchQuery');
jest.mock('../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync');
jest.mock('../../../update/queries');
jest.mock('../../../ora', () => ({
ora: () => ({
@@ -57,6 +59,7 @@ jest.mock('../../../fetch');
describe(UpdateRepublish.name, () => {
afterEach(() => {
vol.reset();
jest.resetAllMocks();
});
it('errors when providing both --group and --branch', async () => {
@@ -89,6 +92,16 @@ describe(UpdateRepublish.name, () => {
);
});
it('errors when providing both --destination-channel and --destination-branch', async () => {
const flags = ['--group=1234', '--destination-channel=test', '--destination-branch=wat'];
mockTestProject();
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
/--destination-branch=wat cannot also be provided when using --destination-channel/
);
});
it('errors when update group is empty', async () => {
const flags = ['--group=1234'];
@@ -271,7 +284,9 @@ describe(UpdateRepublish.name, () => {
mockTestProject();
// Mock resolving the channel to branch name, only valid for a single branch connected
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: updateStub.branch.id, branchName: 'branchFromChannel' });
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
@@ -306,6 +321,94 @@ describe(UpdateRepublish.name, () => {
])
);
});
it('republishes to a different branch when --destination-branch is supplied', async () => {
const flags = ['--branch=branch123', '--message=test-republish', '--destination-branch=other'];
mockTestProject();
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
branch: { id: 'other-id', name: 'other' },
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
jest.mocked(BranchQuery.getBranchByNameAsync).mockResolvedValue({
id: 'other-id',
name: 'other',
});
await new UpdateRepublish(flags, commandOptions).run();
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.objectContaining({ branchName: 'branch123' })
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: 'other-id',
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
});
it('republishes to a different branch when --destination-channel is supplied', async () => {
const flags = ['--branch=branch123', '--message=test-republish', '--destination-channel=blah'];
mockTestProject();
// Mock the prompt to ask the user which update to republish, from branch
jest.mocked(selectUpdateGroupOnBranchAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
branch: { id: 'other-id', name: 'other' },
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: 'blah-id', branchName: 'blah' });
await new UpdateRepublish(flags, commandOptions).run();
expect(selectUpdateGroupOnBranchAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.objectContaining({ branchName: 'branch123' })
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: 'blah-id',
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
});
});
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
@@ -19,7 +19,7 @@ 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 { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { resolveVcsClient } from '../../../vcs';
import UpdateRollBackToEmbedded from '../roll-back-to-embedded';
@@ -50,7 +50,7 @@ jest.mock('../../../project/projectUtils', () => ({
enforceRollBackToEmbeddedUpdateSupportAsync: jest.fn(),
}));
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
@@ -112,7 +112,9 @@ describe(UpdateRollBackToEmbedded.name, () => {
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: updateStub.branch.id, branchName: 'branchFromChannel' });
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
@@ -144,7 +146,16 @@ describe(UpdateRollBackToEmbedded.name, () => {
// Add configuration to the project that should not be included in the update
mockTestProject({
expoConfig: {},
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
const platforms = ['ios'];
+66 -39
View File
@@ -4,6 +4,7 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { transformFingerprintSource } from '../../build/graphql';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
@@ -21,6 +22,7 @@ import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprintAsync';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
RawAsset,
@@ -32,9 +34,9 @@ import {
filterCollectedAssetsByRequestedPlatforms,
generateEasMetadataAsync,
getBranchNameForCommandAsync,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
getRuntimeToUpdateRolloutInfoGroupMappingAsync,
getRuntimeVersionObjectAsync,
getRuntimeVersionInfoObjectsAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
platformDisplayNames,
@@ -125,7 +127,6 @@ export default class UpdatePublish extends EasCommand {
'rollout-percentage': Flags.integer({
description: `Percentage of users this update should be immediately available to. Users not in the rollout will be served the previous latest update on the branch, even if that update is itself being rolled out. The specified number must be an integer between 1 and 100. When not specified, this defaults to 100.`,
required: false,
hidden: true,
min: 0,
max: 100,
}),
@@ -374,7 +375,7 @@ export default class UpdatePublish extends EasCommand {
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersions = await getRuntimeVersionObjectAsync({
const runtimeVersionInfoObjects = await getRuntimeVersionInfoObjectsAsync({
exp,
platforms: realizedPlatforms,
projectDir,
@@ -384,21 +385,40 @@ export default class UpdatePublish extends EasCommand {
},
env: undefined,
});
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
runtimeVersionInfoObjects
);
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping = await Promise.all(
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
return {
...info,
fingerprintSource: info.fingerprint
? (
await maybeUploadFingerprintAsync({
runtimeVersion: info.runtimeVersion,
fingerprint: info.fingerprint,
graphqlClient,
})
).fingerprintSource ?? null
: null,
};
})
);
const runtimeVersionToRolloutInfoGroup =
rolloutPercentage !== undefined
? await getRuntimeToUpdateRolloutInfoGroupMappingAsync(graphqlClient, {
appId: projectId,
branchName,
rolloutPercentage,
runtimeToPlatformMapping,
runtimeToPlatformsAndFingerprintInfoMapping,
})
: undefined;
@@ -406,39 +426,43 @@ export default class UpdatePublish extends EasCommand {
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
const updateGroups: PublishUpdateGroupInput[] =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(
({ runtimeVersion, platforms, fingerprintSource }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
])
);
const rolloutInfoGroupForRuntimeVersion = runtimeVersionToRolloutInfoGroup
? runtimeVersionToRolloutInfoGroup.get(runtimeVersion)
: null;
const localRolloutInfoGroup = rolloutInfoGroupForRuntimeVersion
? Object.fromEntries(
platforms.map(platform => [
platform,
rolloutInfoGroupForRuntimeVersion[platform as keyof UpdateRolloutInfoGroup],
])
)
: null;
const rolloutInfoGroupForRuntimeVersion = runtimeVersionToRolloutInfoGroup
? runtimeVersionToRolloutInfoGroup.get(runtimeVersion)
: null;
const localRolloutInfoGroup = rolloutInfoGroupForRuntimeVersion
? Object.fromEntries(
platforms.map(platform => [
platform,
rolloutInfoGroupForRuntimeVersion[platform as keyof UpdateRolloutInfoGroup],
])
)
: null;
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
rolloutInfoGroup: localRolloutInfoGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
rolloutInfoGroup: localRolloutInfoGroup,
runtimeFingerprintSource: fingerprintSource
? transformFingerprintSource(fingerprintSource)
: null,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
@@ -513,7 +537,10 @@ export default class UpdatePublish extends EasCommand {
Log.addNewLineIfNone();
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
@@ -7,10 +7,11 @@ import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { UpdateToRepublish, republishAsync } from '../../update/republish';
import { truncateString as truncateUpdateMessage } from '../../update/utils';
@@ -22,6 +23,8 @@ const defaultRepublishPlatforms: Platform[] = ['android', 'ios'];
type UpdateRepublishRawFlags = {
branch?: string;
channel?: string;
'destination-channel'?: string;
'destination-branch'?: string;
group?: string;
message?: string;
platform: string;
@@ -33,6 +36,8 @@ type UpdateRepublishRawFlags = {
type UpdateRepublishFlags = {
branchName?: string;
channelName?: string;
destinationChannelName?: string;
destinationBranchName?: string;
groupId?: string;
updateMessage?: string;
platform: Platform[];
@@ -57,6 +62,15 @@ export default class UpdateRepublish extends EasCommand {
description: 'Update group ID to republish',
exclusive: ['branch', 'channel'],
}),
'destination-channel': Flags.string({
description:
'Channel name to select a branch to republish to if republishing to a different branch',
exclusive: ['destination-branch'],
}),
'destination-branch': Flags.string({
description: 'Branch name to republish to if republishing to a different branch',
exclusive: ['destination-channel'],
}),
message: Flags.string({
char: 'm',
description: 'Short message describing the republished update group',
@@ -130,13 +144,21 @@ export default class UpdateRepublish extends EasCommand {
);
}
const updateMessage = await getOrAskUpdateMessageAsync(updatesToPublish, flags);
const arbitraryUpdate = updatesToPublish[0];
const targetBranch = await getOrAskTargetBranchAsync(
graphqlClient,
projectId,
flags,
arbitraryUpdate
);
const updateMessage = await getOrAskUpdateMessageAsync(updatesToPublish, flags);
await republishAsync({
graphqlClient,
app: { exp, projectId },
updatesToPublish,
targetBranch: { branchId: arbitraryUpdate.branchId, branchName: arbitraryUpdate.branchName },
targetBranch,
updateMessage,
codeSigningInfo,
json: flags.json,
@@ -147,6 +169,8 @@ export default class UpdateRepublish extends EasCommand {
const branchName = rawFlags.branch;
const channelName = rawFlags.channel;
const groupId = rawFlags.group;
const destinationChannelName = rawFlags['destination-channel'];
const destinationBranchName = rawFlags['destination-branch'];
const nonInteractive = rawFlags['non-interactive'];
const privateKeyPath = rawFlags['private-key-path'];
@@ -160,6 +184,8 @@ export default class UpdateRepublish extends EasCommand {
return {
branchName,
channelName,
destinationChannelName,
destinationBranchName,
groupId,
platform,
updateMessage: rawFlags.message,
@@ -170,6 +196,34 @@ export default class UpdateRepublish extends EasCommand {
}
}
async function getOrAskTargetBranchAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
flags: UpdateRepublishFlags,
arbitraryUpdate: UpdateToRepublish
): Promise<{ branchId: string; branchName: string }> {
// if branch name supplied, use that
if (flags.destinationBranchName) {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId: projectId,
name: flags.destinationBranchName,
});
return { branchId: branch.id, branchName: branch.name };
}
// if provided channel name but was non-interactive
if (flags.destinationChannelName) {
return await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
flags.destinationChannelName
);
}
// if neither provided, assume republish on same branch
return { branchId: arbitraryUpdate.branchId, branchName: arbitraryUpdate.branchName };
}
/** Retrieve the update group from either the update group id, or select from branch name. */
async function getOrAskUpdatesAsync(
graphqlClient: ExpoGraphqlClient,
@@ -293,7 +347,11 @@ async function askUpdatesFromChannelNameAsync(
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
const branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
const { branchName } = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
channelName
);
return await askUpdatesFromBranchNameAsync(graphqlClient, {
projectId,
@@ -24,11 +24,12 @@ import {
getOwnerAccountForProjectIdAsync,
} from '../../project/projectUtils';
import {
RuntimeVersionInfo,
UpdatePublishPlatform,
defaultPublishPlatforms,
getBranchNameForCommandAsync,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeVersionObjectAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
getRuntimeVersionInfoObjectsAsync,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
@@ -193,7 +194,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersions = await getRuntimeVersionObjectAsync({
const runtimeVersionInfoObjects = await getRuntimeVersionInfoObjectsAsync({
exp,
platforms: realizedPlatforms,
projectDir,
@@ -203,6 +204,10 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
},
env: undefined,
});
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
runtimeVersionInfoObjects
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
@@ -214,7 +219,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
updateMessage,
branchId,
codeSigningInfo,
runtimeVersions,
runtimeToPlatformsAndFingerprintInfoMapping,
realizedPlatforms,
});
publishSpinner.succeed('Published!');
@@ -235,7 +240,10 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
Log.addNewLineIfNone();
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
@@ -290,7 +298,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
updateMessage,
branchId,
codeSigningInfo,
runtimeVersions,
runtimeToPlatformsAndFingerprintInfoMapping,
realizedPlatforms,
}: {
graphqlClient: ExpoGraphqlClient;
@@ -299,17 +307,17 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
updateMessage: string | undefined;
branchId: string;
codeSigningInfo: CodeSigningInfo | undefined;
runtimeVersions: { platform: UpdatePublishPlatform; runtimeVersion: string }[];
runtimeToPlatformsAndFingerprintInfoMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
})[];
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(
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformsAndFingerprintInfoMapping.map(
({ runtimeVersion, platforms }) => {
const localRollbackInfoGroup = Object.fromEntries(
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
@@ -0,0 +1,233 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { WorkerDeploymentAliasFragment } from '../../graphql/generated';
import Log from '../../log';
import { Ora, ora } from '../../ora';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import {
assignWorkerDeploymentAliasAsync,
assignWorkerDeploymentProductionAsync,
selectWorkerDeploymentOnAppAsync,
} from '../../worker/deployment';
import { formatWorkerDeploymentJson, formatWorkerDeploymentTable } from '../../worker/utils/logs';
interface DeployAliasFlags {
nonInteractive: boolean;
json: boolean;
aliasName?: string | null;
deploymentIdentifier?: string | null;
isProduction: boolean;
}
interface RawDeployAliasFlags {
'non-interactive': boolean;
json: boolean;
alias?: string;
id?: string;
prod: boolean;
}
export default class WorkerAlias extends EasCommand {
static override description = 'Assign deployment aliases';
static override aliases = ['deploy:alias', 'deploy:promote'];
// TODO(@kitten): Keep command hidden until worker deployments are live
static override hidden = true;
static override state = 'beta';
static override flags = {
prod: Flags.boolean({
aliases: ['production'],
description: 'Promote an existing deployment to production.',
default: false,
}),
alias: Flags.string({
description: 'Custom alias to assign to the existing deployment.',
helpValue: 'name',
required: false,
}),
id: Flags.string({
description: 'Unique identifier of an existing deployment.',
helpValue: 'xyz123',
required: false,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
override async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(WorkerAlias);
const flags = this.sanitizeFlags(rawFlags);
if (flags.json) {
enableJsonOutput();
}
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkerAlias, {
nonInteractive: true,
});
const { projectId } = await getDynamicPrivateProjectConfigAsync();
const aliasName = await resolveDeploymentAliasAsync(flags);
const deploymentId = await resolveDeploymentIdAsync({
...flags,
graphqlClient,
projectId,
aliasName,
});
let progress: null | Ora = null;
let deploymentAlias: null | Awaited<ReturnType<typeof assignWorkerDeploymentAliasAsync>> = null;
if (aliasName) {
try {
progress = ora(chalk`Assigning alias {bold ${aliasName}} to deployment`).start();
deploymentAlias = await assignWorkerDeploymentAliasAsync({
graphqlClient,
appId: projectId,
deploymentId,
aliasName,
});
progress.text = chalk`Assigned alias {bold ${aliasName}} to deployment`;
} catch (error: any) {
progress?.fail(chalk`Failed to assign {bold ${aliasName}} alias to deployment`);
throw error;
}
}
let deploymentProdAlias: null | Awaited<
ReturnType<typeof assignWorkerDeploymentProductionAsync>
> = null;
if (flags.isProduction) {
try {
progress = (progress ?? ora()).start(chalk`Promoting deployment to {bold production}`);
deploymentProdAlias = await assignWorkerDeploymentProductionAsync({
graphqlClient,
appId: projectId,
deploymentId,
});
progress.text = chalk`Promoted deployment to {bold production}`;
} catch (error: any) {
progress?.fail(chalk`Failed to promote deployment to {bold production}`);
throw error;
}
}
progress?.succeed(
!deploymentAlias
? chalk`Promoted deployment to {bold production}`
: chalk`Promoted deployment to {bold production} with alias {bold ${deploymentAlias.aliasName}}`
);
// Either use the alias, or production deployment information
const deployment = deploymentAlias?.workerDeployment ?? deploymentProdAlias?.workerDeployment;
if (flags.json) {
printJsonOnlyOutput(
formatWorkerDeploymentJson({
projectId,
deployment: deployment!,
aliases: [deploymentAlias].filter(Boolean) as WorkerDeploymentAliasFragment[],
production: deploymentProdAlias,
})
);
return;
}
Log.addNewLineIfNone();
Log.log(`🎉 Your deployment is modified`);
Log.addNewLineIfNone();
Log.log(
formatWorkerDeploymentTable({
projectId,
deployment: deployment!,
aliases: [deploymentAlias].filter(Boolean) as WorkerDeploymentAliasFragment[],
production: deploymentProdAlias,
})
);
}
private sanitizeFlags(flags: RawDeployAliasFlags): DeployAliasFlags {
return {
nonInteractive: flags['non-interactive'],
json: flags['json'],
aliasName: flags.alias?.trim().toLowerCase(),
deploymentIdentifier: flags.id?.trim().toLowerCase(),
isProduction: flags.prod,
};
}
}
async function resolveDeploymentAliasAsync(flags: DeployAliasFlags): Promise<string | null> {
if (flags.aliasName?.trim()) {
return flags.aliasName.trim().toLowerCase();
}
// Skip alias prompt when promoting deployments to prod
if (flags.isProduction) {
return null;
}
if (flags.nonInteractive) {
throw new Error('The `--alias` flag must be set when running in `--non-interactive` mode.');
}
const { alias: aliasName } = await promptAsync({
type: 'text',
name: 'alias',
message: 'Enter the alias to assign to a deployment',
validate: (value: string) => !!value.trim(),
hint: 'The alias name is case insensitive and must be URL safe',
});
return aliasName.trim().toLowerCase();
}
async function resolveDeploymentIdAsync({
graphqlClient,
deploymentIdentifier,
aliasName,
projectId,
nonInteractive,
}: DeployAliasFlags & {
graphqlClient: ExpoGraphqlClient;
projectId: string;
}): Promise<string> {
if (deploymentIdentifier) {
return deploymentIdentifier;
}
if (nonInteractive) {
throw new Error('The `--id` flag must be set when running in `--non-interactive` mode.');
}
const deployment = await selectWorkerDeploymentOnAppAsync({
graphqlClient,
appId: projectId,
selectTitle: chalk`deployment to assign the {underline ${aliasName}} alias`,
});
if (!deployment) {
throw new Error(
'No deployments found for this project, create a new deployment with "eas deploy"'
);
}
return deployment.deploymentIdentifier;
}
@@ -0,0 +1,442 @@
import { format as formatTimeAgo } from '@expo/timeago.js';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import fs from 'node:fs';
import * as path from 'node:path';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import Log from '../../log';
import { ora } from '../../ora';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { createProgressTracker } from '../../utils/progress';
import * as WorkerAssets from '../../worker/assets';
import {
assignWorkerDeploymentAliasAsync,
assignWorkerDeploymentProductionAsync,
getSignedDeploymentUrlAsync,
} from '../../worker/deployment';
import {
UploadParams,
batchUploadAsync,
callUploadApiAsync,
uploadAsync,
} from '../../worker/upload';
import {
formatWorkerDeploymentJson,
formatWorkerDeploymentTable,
getDeploymentUrlFromFullName,
} from '../../worker/utils/logs';
const isDirectory = (directoryPath: string): Promise<boolean> =>
fs.promises
.stat(directoryPath)
.then(stat => stat.isDirectory())
.catch(() => false);
interface DeployFlags {
nonInteractive: boolean;
json: boolean;
isProduction: boolean;
aliasName?: string;
environment?: EnvironmentVariableEnvironment;
deploymentIdentifier?: string;
exportDir: string;
}
interface RawDeployFlags {
'non-interactive': boolean;
environment?: string;
json: boolean;
prod: boolean;
alias?: string;
id?: string;
'export-dir': string;
}
interface DeployInProgressParams {
id: string;
fullName: string;
baseURL: string;
token: string;
}
export default class WorkerDeploy extends EasCommand {
static override description = 'Deploy your Expo web build';
static override aliases = ['deploy'];
static override usage = [chalk`deploy {dim [options]}`, `deploy --prod`];
// TODO(@kitten): Keep command hidden until worker deployments are live
static override hidden = true;
static override state = 'beta';
static override flags = {
prod: Flags.boolean({
aliases: ['production'],
description: 'Create a new production deployment.',
default: false,
}),
alias: Flags.string({
description: 'Custom alias to assign to the new deployment.',
helpValue: 'name',
}),
id: Flags.string({
description: 'Custom unique identifier for the new deployment.',
helpValue: 'xyz123',
}),
'export-dir': Flags.string({
description: 'Directory where the Expo project was exported.',
helpValue: 'dir',
default: 'dist',
}),
environment: {
...EASEnvironmentFlag.environment,
description: 'Deploy with EAS Environment Variables matching the specified environment.',
},
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(WorkerDeploy);
const flags = this.sanitizeFlags(rawFlags);
if (flags.json) {
enableJsonOutput();
}
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
projectDir,
} = await this.getContextAsync(WorkerDeploy, flags);
const projectDist = await resolveExportedProjectAsync(flags, projectDir);
const { projectId } = await getDynamicPrivateProjectConfigAsync();
logExportedProjectInfo(projectDist);
async function* emitWorkerTarballAsync(params: {
assetMap: WorkerAssets.AssetMap;
manifest: WorkerAssets.Manifest;
}): AsyncGenerator<WorkerAssets.FileEntry> {
yield ['assets.json', JSON.stringify(params.assetMap)];
yield ['manifest.json', JSON.stringify(params.manifest)];
if (projectDist.type === 'server' && projectDist.serverPath) {
const workerFiles = WorkerAssets.listWorkerFilesAsync(projectDist.serverPath);
for await (const workerFile of workerFiles) {
yield [`server/${workerFile.normalizedPath}`, workerFile.data];
}
}
}
async function finalizeDeployAsync(deployParams: DeployInProgressParams): Promise<void> {
const finalizeDeployUrl = new URL('/deploy/finalize', deployParams.baseURL);
finalizeDeployUrl.searchParams.set('token', deployParams.token);
const result = await callUploadApiAsync(finalizeDeployUrl, {
method: 'POST',
headers: {
accept: 'application/json',
},
});
if (!result || typeof result !== 'object' || !('success' in result) || !result.success) {
throw new Error('Deploy failed: Incomplete asset uploads. Please try again');
}
}
async function uploadTarballAsync(
tarPath: string,
uploadUrl: string
): Promise<DeployInProgressParams> {
const { response } = await uploadAsync({
url: uploadUrl,
filePath: tarPath,
compress: false,
headers: {
accept: 'application/json',
},
});
if (response.status === 413) {
throw new Error(
'Upload failed! (Payload too large)\n' +
`The files in "${path.relative(
projectDir,
projectDist.path
)}" (at: ${projectDir}) exceed the maximum file size (10MB gzip).`
);
} else if (!response.ok) {
throw new Error(`Upload failed! (${response.statusText})`);
} else {
const json = await response.json();
if (!json.success || !json.result || typeof json.result !== 'object') {
throw new Error(json.message ? `Upload failed: ${json.message}` : 'Upload failed!');
}
const { id, fullName, token } = json.result;
if (typeof token !== 'string') {
throw new Error('Upload failed: API failed to return a deployment token');
} else if (typeof id !== 'string') {
throw new Error('Upload failed: API failed to return a deployment identifier');
} else if (typeof fullName !== 'string') {
throw new Error('Upload failed: API failed to return a script name');
}
const baseURL = new URL('/', uploadUrl).toString();
return { id, fullName, baseURL, token };
}
}
async function uploadAssetsAsync(
assetMap: WorkerAssets.AssetMap,
deployParams: DeployInProgressParams
): Promise<void> {
const uploadParams: UploadParams[] = [];
const assetPath = projectDist.type === 'server' ? projectDist.clientPath : projectDist.path;
if (!assetPath) {
return;
}
for await (const asset of WorkerAssets.listAssetMapFilesAsync(assetPath, assetMap)) {
const uploadURL = new URL(`/asset/${asset.sha512}`, deployParams.baseURL);
uploadURL.searchParams.set('token', deployParams.token);
uploadParams.push({ url: uploadURL.toString(), filePath: asset.path });
}
const progress = {
total: uploadParams.length,
pending: 0,
percent: 0,
transferred: 0,
};
const updateProgress = createProgressTracker({
total: progress.total,
message(ratio) {
const percent = `${Math.floor(ratio * 100)}`;
const details = chalk.dim(
`(${progress.pending} Pending, ${progress.transferred} Completed, ${progress.total} Total)`
);
return `Uploading assets: ${percent.padStart(3)}% ${details}`;
},
completedMessage: 'Uploaded assets',
});
try {
for await (const signal of batchUploadAsync(uploadParams)) {
if ('response' in signal) {
progress.pending--;
progress.percent = ++progress.transferred / progress.total;
} else {
progress.pending++;
}
updateProgress({ progress });
}
} catch (error: any) {
updateProgress({ isComplete: true, error });
throw error;
}
updateProgress({ isComplete: true });
}
let assetMap: WorkerAssets.AssetMap;
let tarPath: string;
let deployResult: DeployInProgressParams;
let progress = ora('Preparing project').start();
try {
const manifest = await WorkerAssets.createManifestAsync(
{
environment: flags.environment,
projectDir,
projectId,
},
graphqlClient
);
assetMap = await WorkerAssets.createAssetMapAsync(
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path
);
tarPath = await WorkerAssets.packFilesIterableAsync(
emitWorkerTarballAsync({
assetMap,
manifest,
})
);
const uploadUrl = await getSignedDeploymentUrlAsync(graphqlClient, {
appId: projectId,
deploymentIdentifier: flags.deploymentIdentifier,
// NOTE(cedric): this function might ask the user for a dev-domain name,
// when that happens, no ora spinner should be running.
onSetupDevDomain: () => progress.stop(),
nonInteractive: flags.nonInteractive,
});
progress.start('Creating deployment');
deployResult = await uploadTarballAsync(tarPath, uploadUrl);
progress.succeed('Created deployment');
} catch (error: any) {
progress.fail('Failed to create deployment');
throw error;
}
await uploadAssetsAsync(assetMap, deployResult);
await finalizeDeployAsync(deployResult);
let deploymentAlias: null | Awaited<ReturnType<typeof assignWorkerDeploymentAliasAsync>> = null;
if (flags.aliasName) {
progress = ora(chalk`Assigning alias {bold ${flags.aliasName}} to deployment`).start();
try {
deploymentAlias = await assignWorkerDeploymentAliasAsync({
graphqlClient,
appId: projectId,
deploymentId: deployResult.id,
aliasName: flags.aliasName,
});
// Only stop the spinner when not promoting to production
if (!flags.isProduction) {
progress.succeed(chalk`Assigned alias {bold ${flags.aliasName}} to deployment`);
}
} catch (error: any) {
progress.fail(chalk`Failed to assign {bold ${flags.aliasName}} alias to deployment`);
throw error;
}
}
let deploymentProdAlias: null | Awaited<
ReturnType<typeof assignWorkerDeploymentProductionAsync>
> = null;
if (flags.isProduction) {
try {
if (!flags.aliasName) {
progress = ora(chalk`Promoting deployment to {bold production}`).start();
} else {
progress.text = chalk`Promoting deployment to {bold production}`;
}
deploymentProdAlias = await assignWorkerDeploymentProductionAsync({
graphqlClient,
appId: projectId,
deploymentId: deployResult.id,
});
progress.succeed(
!flags.aliasName
? chalk`Promoted deployment to {bold production}`
: chalk`Promoted deployment to {bold production} with alias {bold ${flags.aliasName}}`
);
} catch (error: any) {
progress.fail('Failed to promote deployment to production');
throw error;
}
}
if (flags.json) {
printJsonOnlyOutput(
formatWorkerDeploymentJson({
projectId,
deployment: {
deploymentIdentifier: deployResult.id,
url: getDeploymentUrlFromFullName(deployResult.fullName),
},
aliases: [deploymentAlias],
production: deploymentProdAlias,
})
);
return;
}
Log.addNewLineIfNone();
Log.log(`🎉 Your deployment is ready`);
Log.addNewLineIfNone();
Log.log(
formatWorkerDeploymentTable({
projectId,
deployment: {
deploymentIdentifier: deployResult.id,
url: getDeploymentUrlFromFullName(deployResult.fullName),
},
aliases: [deploymentAlias],
production: deploymentProdAlias,
})
);
if (!deploymentProdAlias) {
Log.addNewLineIfNone();
Log.log('🚀 When you are ready to deploy to production:');
Log.log(chalk` $ eas deploy {bold --prod}`);
}
}
private sanitizeFlags(flags: RawDeployFlags): DeployFlags {
return {
nonInteractive: flags['non-interactive'],
json: flags['json'],
isProduction: !!flags.prod,
aliasName: flags.alias?.trim().toLowerCase(),
deploymentIdentifier: flags.id?.trim(),
exportDir: flags['export-dir'],
};
}
}
async function resolveExportedProjectAsync(
flags: DeployFlags,
projectDir: string
): Promise<
| { type: 'static'; modifiedAt: Date | null; path: string }
| {
type: 'server';
modifiedAt: Date | null;
path: string;
serverPath: string;
clientPath?: string;
}
> {
const exportPath = path.join(projectDir, flags.exportDir);
const serverPath = path.join(exportPath, 'server');
const clientPath = path.join(exportPath, 'client');
const [exportDirStat, expoRoutesStat, hasClientDir] = await Promise.all([
fs.promises.stat(exportPath).catch(() => null),
fs.promises.stat(path.join(serverPath, '_expo/routes.json')).catch(() => null),
isDirectory(clientPath),
]);
if (!exportDirStat?.isDirectory()) {
throw new Error(
`No "${flags.exportDir}/" folder found. Prepare your project for deployment with "npx expo export --platform web"`
);
}
if (expoRoutesStat?.isFile()) {
return {
type: 'server',
path: exportPath,
modifiedAt: exportDirStat.mtime,
serverPath,
clientPath: hasClientDir ? clientPath : undefined,
};
}
return { type: 'static', path: exportPath, modifiedAt: exportDirStat.mtime };
}
function logExportedProjectInfo(
project: Awaited<ReturnType<typeof resolveExportedProjectAsync>>
): void {
let modifiedAgo = '';
// Only show the timestamp for exports older than 1 minute
if (project.modifiedAt && Date.now() - project.modifiedAt.getTime() > 60_000) {
modifiedAgo = ` - exported ${formatTimeAgo(project.modifiedAt)}`;
}
Log.log(chalk`{dim > Project export: ${project.type}${modifiedAgo}}`);
}
@@ -747,7 +747,47 @@ export const CapabilityMapping: CapabilityClassifier[] = [
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
entitlement: 'com.apple.developer.journal.allow',
name: 'Journaling Suggestions',
capability: CapabilityType.JOURNALING_SUGGESTIONS,
validateOptions: createValidateStringArrayOptions(['suggestions']),
getOptions: getDefinedOptions,
},
{
entitlement: 'com.apple.developer.managed-app-distribution.install-ui',
name: 'Managed App Installation UI',
capability: CapabilityType.MANAGED_APP_INSTALLATION_UI,
validateOptions: createValidateStringArrayOptions(['managed-app']),
getOptions: getDefinedOptions,
},
{
entitlement: 'com.apple.developer.networking.slicing.appcategory',
name: '5G Network Slicing',
capability: CapabilityType.NETWORK_SLICING,
validateOptions: createValidateStringArrayOptions([
'gaming-6014',
'communication-9000',
'streaming-9001',
]),
getOptions: getDefinedOptions,
},
{
entitlement: 'com.apple.developer.networking.slicing.trafficcategory',
name: '5G Network Slicing',
capability: CapabilityType.NETWORK_SLICING,
validateOptions: createValidateStringArrayOptions([
'defaultslice-1',
'video-2',
'background-3',
'voice-4',
'callsignaling-5',
'responsivedata-6',
'avstreaming-7',
'responsiveav-8',
]),
getOptions: getDefinedOptions,
},
// VMNET
// These don't appear to have entitlements, so it's unclear how we can automatically enable / disable them at this time.
@@ -36,6 +36,7 @@ export enum AndroidActionType {
UpdateCredentialsJson,
SetUpBuildCredentialsFromCredentialsJson,
SetUpBuildCredentials,
Exit,
}
export enum IosActionType {
@@ -60,4 +61,5 @@ export enum IosActionType {
UseExistingAscApiKeyForSubmissions,
CreateAscApiKeyForSubmissions,
RemoveAscApiKey,
Exit,
}
@@ -26,6 +26,11 @@ export const highLevelActions: ActionInfo[] = [
title: 'Go back',
scope: Scope.Manager,
},
{
value: AndroidActionType.Exit,
title: 'Exit',
scope: Scope.Manager,
},
];
export const credentialsJsonActions: ActionInfo[] = [
@@ -27,6 +27,11 @@ export const highLevelActions: ActionInfo[] = [
title: 'Go back',
scope: Scope.Manager,
},
{
value: IosActionType.Exit,
title: 'Exit',
scope: Scope.Manager,
},
];
export const credentialsJsonActions: ActionInfo[] = [
@@ -145,6 +145,8 @@ export class ManageAndroid {
} else if (chosenAction === AndroidActionType.GoBackToCaller) {
await this.callingAction.runAsync(ctx);
return;
} else if (chosenAction === AndroidActionType.Exit) {
return;
}
}
@@ -154,6 +154,8 @@ export class ManageIos {
} else if (chosenAction === IosActionType.GoBackToCaller) {
await this.callingAction.runAsync(ctx);
return;
} else if (chosenAction === IosActionType.Exit) {
return;
}
} else if (actionInfo.scope === Scope.Project) {
assert(
+859 -135
View File
File diff suppressed because it is too large. Load diff
@@ -4,21 +4,26 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
CreateBulkEnvironmentVariablesForAppMutation,
CreateEnvironmentVariableForAccountMutation,
CreateEnvironmentVariableForAppMutation,
DeleteEnvironmentVariableMutation,
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableVisibility,
LinkSharedEnvironmentVariableMutation,
UnlinkSharedEnvironmentVariableMutation,
UpdateEnvironmentVariableMutation,
} from '../generated';
import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
type UpdateVariableArgs = {
value?: string;
type CreateVariableArgs = {
value: string;
name: string;
overwrite: true;
visibility?: EnvironmentVariableVisibility;
visibility: EnvironmentVariableVisibility;
environments: EnvironmentVariableEnvironment[];
type: EnvironmentSecretType;
};
export type EnvironmentVariablePushInput = {
@@ -34,7 +39,7 @@ export const EnvironmentVariableMutation = {
graphqlClient: ExpoGraphqlClient,
environmentVariableId: string,
appId: string,
environment: string
environment?: EnvironmentVariableEnvironment
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -42,7 +47,7 @@ export const EnvironmentVariableMutation = {
gql`
mutation LinkSharedEnvironmentVariable(
$appId: ID!
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
$environmentVariableId: ID!
) {
environmentVariable {
@@ -69,7 +74,7 @@ export const EnvironmentVariableMutation = {
graphqlClient: ExpoGraphqlClient,
environmentVariableId: string,
appId: string,
environment: string
environment?: EnvironmentVariableEnvironment
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -77,7 +82,7 @@ export const EnvironmentVariableMutation = {
gql`
mutation UnlinkSharedEnvironmentVariable(
$appId: ID!
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
$environmentVariableId: ID!
) {
environmentVariable {
@@ -102,14 +107,7 @@ export const EnvironmentVariableMutation = {
},
async createSharedVariableAsync(
graphqlClient: ExpoGraphqlClient,
input:
| {
name: string;
value: string;
visibility: EnvironmentVariableVisibility;
overwrite?: boolean;
}
| UpdateVariableArgs,
input: CreateVariableArgs,
accountId: string
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
@@ -141,15 +139,7 @@ export const EnvironmentVariableMutation = {
},
async createForAppAsync(
graphqlClient: ExpoGraphqlClient,
input:
| {
name: string;
value?: string;
environment: string;
visibility: EnvironmentVariableVisibility;
overwrite?: boolean;
}
| (UpdateVariableArgs & { environment: string }),
input: CreateVariableArgs,
appId: string
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
@@ -176,6 +166,31 @@ export const EnvironmentVariableMutation = {
return data.environmentVariable.createEnvironmentVariableForApp;
},
async updateAsync(
graphqlClient: ExpoGraphqlClient,
input: Partial<CreateVariableArgs> & { id: string }
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateEnvironmentVariableMutation>(
gql`
mutation UpdateEnvironmentVariable($input: UpdateEnvironmentVariableInput!) {
environmentVariable {
updateEnvironmentVariable(environmentVariableData: $input) {
id
...EnvironmentVariableFragment
}
}
}
${print(EnvironmentVariableFragmentNode)}
`,
{ input }
)
.toPromise()
);
return data.environmentVariable.updateEnvironmentVariable;
},
async deleteAsync(graphqlClient: ExpoGraphqlClient, id: string): Promise<{ id: string }> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -196,4 +211,34 @@ export const EnvironmentVariableMutation = {
return data.environmentVariable.deleteEnvironmentVariable;
},
async createBulkEnvironmentVariablesForAppAsync(
graphqlClient: ExpoGraphqlClient,
input: EnvironmentVariablePushInput[],
appId: string
): Promise<boolean> {
await withErrorHandlingAsync(
graphqlClient
.mutation<CreateBulkEnvironmentVariablesForAppMutation>(
gql`
mutation CreateBulkEnvironmentVariablesForApp(
$input: [CreateEnvironmentVariableInput!]!
$appId: ID!
) {
environmentVariable {
createBulkEnvironmentVariablesForApp(
environmentVariablesData: $input
appId: $appId
) {
id
}
}
}
`,
{ input, appId }
)
.toPromise()
);
return true;
},
};
@@ -0,0 +1,50 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
AppStoreConnectApiKeyByIdQuery,
AppStoreConnectApiKeyByIdQueryVariables,
} from '../generated';
export const AppStoreConnectApiKeyQuery = {
async getByIdAsync(
graphqlClient: ExpoGraphqlClient,
ascApiKeyId: string
): Promise<{
issuerIdentifier: string;
keyIdentifier: string;
keyP8: string;
}> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppStoreConnectApiKeyByIdQuery, AppStoreConnectApiKeyByIdQueryVariables>(
gql`
query AppStoreConnectApiKeyById($ascApiKeyId: ID!) {
appStoreConnectApiKey {
byId(id: $ascApiKeyId) {
id
issuerIdentifier
keyIdentifier
keyP8
}
}
}
`,
{ ascApiKeyId },
{
additionalTypenames: ['AppStoreConnectApiKey'],
}
)
.toPromise()
);
const key = data.appStoreConnectApiKey.byId;
return {
issuerIdentifier: key.issuerIdentifier,
keyIdentifier: key.keyIdentifier,
keyP8: key.keyP8,
};
},
};
@@ -12,6 +12,10 @@ import {
} from '../generated';
import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
type EnvironmentVariableWithLinkedEnvironments = EnvironmentVariableFragment & {
linkedEnvironments?: EnvironmentVariableEnvironment[] | null;
};
export const EnvironmentVariablesQuery = {
async byAppIdWithSensitiveAsync(
graphqlClient: ExpoGraphqlClient,
@@ -21,12 +25,10 @@ export const EnvironmentVariablesQuery = {
filterNames,
}: {
appId: string;
environment: EnvironmentVariableEnvironment;
environment?: EnvironmentVariableEnvironment;
filterNames?: string[];
}
): Promise<{
appVariables: EnvironmentVariableFragment[];
}> {
): Promise<EnvironmentVariableFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
@@ -34,7 +36,7 @@ export const EnvironmentVariablesQuery = {
query EnvironmentVariablesIncludingSensitiveByAppId(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
@@ -46,27 +48,32 @@ export const EnvironmentVariablesQuery = {
id
name
value
environments
}
}
}
}
`,
{ appId, filterNames, environment },
{ additionalTypenames: ['EnvironmentVariable'] }
{ additionalTypenames: ['EnvironmentVariableWithSecret'] }
)
.toPromise()
);
return {
appVariables: data.app?.byId.environmentVariablesIncludingSensitive ?? [],
};
return data.app?.byId.environmentVariablesIncludingSensitive ?? [];
},
async byAppIdAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
environment: string,
filterNames?: string[]
): Promise<EnvironmentVariableFragment[]> {
{
appId,
environment,
filterNames,
}: {
appId: string;
environment?: EnvironmentVariableEnvironment;
filterNames?: string[];
}
): Promise<EnvironmentVariableWithLinkedEnvironments[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<EnvironmentVariablesByAppIdQuery>(
@@ -74,13 +81,14 @@ export const EnvironmentVariablesQuery = {
query EnvironmentVariablesByAppId(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
id
environmentVariables(filterNames: $filterNames, environment: $environment) {
id
linkedEnvironments(appId: $appId)
...EnvironmentVariableFragment
}
}
@@ -98,21 +106,29 @@ export const EnvironmentVariablesQuery = {
},
async sharedAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
filterNames?: string[]
): Promise<EnvironmentVariableFragment[]> {
{
appId,
filterNames,
environment,
}: { appId: string; filterNames?: string[]; environment?: EnvironmentVariableEnvironment }
): Promise<EnvironmentVariableWithLinkedEnvironments[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<EnvironmentVariablesSharedQuery, EnvironmentVariablesSharedQueryVariables>(
gql`
query EnvironmentVariablesShared($appId: String!, $filterNames: [String!]) {
query EnvironmentVariablesShared(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
id
ownerAccount {
id
environmentVariables(filterNames: $filterNames) {
environmentVariables(filterNames: $filterNames, environment: $environment) {
id
linkedEnvironments(appId: $appId)
...EnvironmentVariableFragment
}
}
@@ -121,7 +137,7 @@ export const EnvironmentVariablesQuery = {
}
${print(EnvironmentVariableFragmentNode)}
`,
{ appId, filterNames },
{ appId, filterNames, environment },
{ additionalTypenames: ['EnvironmentVariable'] }
)
.toPromise()
@@ -129,4 +145,48 @@ export const EnvironmentVariablesQuery = {
return data.app?.byId.ownerAccount.environmentVariables ?? [];
},
async sharedWithSensitiveAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
filterNames,
environment,
}: { appId: string; filterNames?: string[]; environment?: EnvironmentVariableEnvironment }
): Promise<EnvironmentVariableFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
gql`
query EnvironmentVariablesSharedWithSensitive(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
id
ownerAccount {
id
environmentVariablesIncludingSensitive(
filterNames: $filterNames
environment: $environment
) {
id
name
value
environments
}
}
}
}
}
`,
{ appId, filterNames, environment },
{ additionalTypenames: ['EnvironmentVariableWithSecret'] }
)
.toPromise()
);
return data.app?.byId.ownerAccount.environmentVariablesIncludingSensitive ?? [];
},
};
@@ -0,0 +1,38 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
GoogleServiceAccountKeyByIdQuery,
GoogleServiceAccountKeyByIdQueryVariables,
} from '../generated';
export const GoogleServiceAccountKeyQuery = {
async getByIdAsync(
graphqlClient: ExpoGraphqlClient,
ascApiKeyId: string
): Promise<{ keyJson: string }> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<GoogleServiceAccountKeyByIdQuery, GoogleServiceAccountKeyByIdQueryVariables>(
gql`
query GoogleServiceAccountKeyById($ascApiKeyId: ID!) {
googleServiceAccountKey {
byId(id: $ascApiKeyId) {
id
keyJson
}
}
}
`,
{ ascApiKeyId },
{
additionalTypenames: ['GoogleServiceAccountKey'],
}
)
.toPromise()
);
return { keyJson: data.googleServiceAccountKey.byId.keyJson };
},
};
@@ -5,8 +5,9 @@ export const EnvironmentVariableFragmentNode = gql`
id
name
value
environment
environments
createdAt
updatedAt
scope
visibility
}
@@ -28,6 +28,6 @@ export const SubmissionFragmentNode = gql`
errorCode
message
}
logsUrl
logFiles
}
`;
@@ -46,17 +46,5 @@ describe('expoConfig', () => {
await createOrModifyExpoConfigAsync('/app', { owner: 'ccheever' });
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', { owner: 'ccheever' });
});
it('adds expo key if app.json is missing it', async () => {
jest.mocked(getConfigFilePaths).mockReturnValue({
staticConfigPath: '/app/app.json',
dynamicConfigPath: null,
});
jest.mocked(JsonFile.readAsync).mockResolvedValue({ name: 'App' });
await createOrModifyExpoConfigAsync('/app', { owner: 'ccheever' });
expect(JsonFile.writeAsync).toHaveBeenCalledWith('/app/app.json', { name: 'App', expo: {} });
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', { owner: 'ccheever' });
});
});
});
@@ -65,9 +65,6 @@ export async function getApplicationIdFromBareAsync(
projectDir: string,
gradleContext?: GradleBuildContext
): Promise<string> {
if (env.overrideAndroidApplicationId) {
return env.overrideAndroidApplicationId;
}
const errorMessage = 'Could not read applicationId from Android project.';
if (gradleContext) {
@@ -109,6 +106,10 @@ export async function getApplicationIdAsync(
vcsClient: Client,
gradleContext?: GradleBuildContext
): Promise<string> {
if (env.overrideAndroidApplicationId) {
return env.overrideAndroidApplicationId;
}
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import {
@@ -26,6 +26,7 @@ export async function getApplicationIdentifierAsync({
platform,
vcsClient,
nonInteractive,
env,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
@@ -35,6 +36,7 @@ export async function getApplicationIdentifierAsync({
platform: Platform;
vcsClient: Client;
nonInteractive: boolean;
env: Env;
}): Promise<string> {
if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>;
@@ -71,11 +73,12 @@ export async function getApplicationIdentifierAsync({
{ exp, projectDir, nonInteractive: false, vcsClient },
profile
);
const targets = await resolveTargetsAsync({
projectDir,
exp,
xcodeBuildContext,
env: profile.env,
env,
vcsClient,
});
const applicationTarget = findApplicationTarget(targets);
+2 -17
View File
@@ -1,9 +1,7 @@
import { ExpoConfig, getConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import fs from 'fs-extra';
import Joi from 'joi';
import nullthrows from 'nullthrows';
import path from 'path';
export type PublicExpoConfig = Omit<
@@ -18,6 +16,7 @@ export type PublicExpoConfig = Omit<
export interface ExpoConfigOptions {
env?: Env;
skipSDKVersionRequirement?: boolean;
skipPlugins?: boolean;
}
interface ExpoConfigOptionsInternal extends ExpoConfigOptions {
@@ -30,7 +29,6 @@ export async function createOrModifyExpoConfigAsync(
readOptions?: { skipSDKVersionRequirement?: boolean }
): ReturnType<typeof modifyConfigAsync> {
ensureExpoConfigExists(projectDir);
await ensureStaticExpoConfigIsValidAsync(projectDir);
if (readOptions) {
return await modifyConfigAsync(projectDir, exp, readOptions);
@@ -52,6 +50,7 @@ function getExpoConfigInternal(
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
...(opts.skipPlugins ? { skipPlugins: true } : {}),
});
const { error } = MinimalAppConfigSchema.validate(exp, {
@@ -93,20 +92,6 @@ export function ensureExpoConfigExists(projectDir: string): void {
}
}
async function ensureStaticExpoConfigIsValidAsync(projectDir: string): Promise<void> {
if (isUsingStaticExpoConfig(projectDir)) {
const staticConfigPath = nullthrows(getConfigFilePaths(projectDir).staticConfigPath);
const staticConfig = await JsonFile.readAsync(staticConfigPath);
// Add the "expo" key if it doesn't exist on app.json yet, such as in
// projects initialized with RNC CLI
if (!staticConfig?.expo) {
staticConfig.expo = {};
await JsonFile.writeAsync(staticConfigPath, staticConfig);
}
}
}
export function isUsingStaticExpoConfig(projectDir: string): boolean {
const paths = getConfigFilePaths(projectDir);
return !!(paths.staticConfigPath?.endsWith('app.json') && !paths.dynamicConfigPath);
@@ -59,14 +59,15 @@ export async function getBundleIdentifierAsync(
vcsClient: Client,
xcodeContext?: { targetName?: string; buildConfiguration?: string }
): Promise<string> {
if (env.overrideIosBundleIdentifier) {
return env.overrideIosBundleIdentifier;
}
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
if (workflow === Workflow.GENERIC) {
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
if (env.overrideIosBundleIdentifier) {
return env.overrideIosBundleIdentifier;
}
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
const isMultiScheme = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir).length > 1;
const isMultiTarget =
+1 -1
View File
@@ -189,7 +189,7 @@ async function resolveBareProjectDependenciesAsync({
}
);
const entitlements = await getNativeTargetEntitlementsAsync(projectDir, {
targetName: target.name,
targetName: dependency.name,
buildConfiguration,
});
result.push({
@@ -0,0 +1,79 @@
import { FingerprintSource, FingerprintSourceType } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { LocalBuildMode } from '../build/local';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { UploadSessionType } from '../graphql/generated';
import Log from '../log';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { getTmpDirectory } from '../utils/paths';
export async function maybeUploadFingerprintAsync({
runtimeVersion,
fingerprint,
graphqlClient,
localBuildMode,
}: {
runtimeVersion: string;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
};
graphqlClient: ExpoGraphqlClient;
localBuildMode?: LocalBuildMode;
}): Promise<{
runtimeVersion: string;
fingerprintSource?: FingerprintSource;
}> {
await fs.mkdirp(getTmpDirectory());
const fingerprintLocation = path.join(getTmpDirectory(), `${uuidv4()}-runtime-fingerprint.json`);
await fs.writeJSON(fingerprintLocation, {
hash: runtimeVersion,
sources: fingerprint.fingerprintSources,
});
if (localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
runtimeVersion,
fingerprintSource: {
type: FingerprintSourceType.PATH,
path: fingerprintLocation,
isDebugFingerprint: fingerprint.isDebugFingerprintSource,
},
};
}
let fingerprintGCSBucketKey = undefined;
try {
fingerprintGCSBucketKey = await uploadFileAtPathToGCSAsync(
graphqlClient,
UploadSessionType.EasUpdateFingerprint,
fingerprintLocation
);
} catch (err: any) {
let errMessage = 'Failed to upload fingerprint to EAS';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return {
runtimeVersion,
};
} finally {
await fs.remove(fingerprintLocation);
}
return {
runtimeVersion,
fingerprintSource: {
type: FingerprintSourceType.GCS,
bucketKey: fingerprintGCSBucketKey,
isDebugFingerprint: fingerprint.isDebugFingerprintSource,
},
};
}
+81 -49
View File
@@ -13,6 +13,7 @@ import path from 'path';
import promiseLimit from 'promise-limit';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from './projectUtils';
import { resolveRuntimeVersionUsingCLIAsync } from './resolveRuntimeVersionAsync';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getDefaultBranchNameAsync } from '../branch/utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -30,7 +31,7 @@ import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log, { learnMore } from '../log';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import {
UpdateJsonInfo,
formatUpdateMessage,
@@ -42,12 +43,10 @@ import {
shouldUseVersionedExpoCLI,
shouldUseVersionedExpoCLIWithExplicitPlatforms,
} from '../utils/expoCli';
import {
ExpoUpdatesCLIModuleNotFoundError,
expoUpdatesCommandAsync,
} from '../utils/expoUpdatesCli';
import { ExpoUpdatesCLIModuleNotFoundError } from '../utils/expoUpdatesCli';
import chunk from '../utils/expodash/chunk';
import { truthy } from '../utils/expodash/filter';
import groupBy from '../utils/expodash/groupBy';
import uniqBy from '../utils/expodash/uniqBy';
import { Client } from '../vcs/vcs';
@@ -599,7 +598,12 @@ export async function getBranchNameForCommandAsync({
}
if (channelNameArg) {
return await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelNameArg);
const { branchName } = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
channelNameArg
);
return branchName;
}
if (branchNameArg) {
@@ -698,7 +702,15 @@ export async function getUpdateMessageForCommandAsync(
export const defaultPublishPlatforms: UpdatePublishPlatform[] = ['android', 'ios'];
export async function getRuntimeVersionObjectAsync({
export type RuntimeVersionInfo = {
runtimeVersion: string;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
};
export async function getRuntimeVersionInfoObjectsAsync({
exp,
platforms,
workflows,
@@ -710,12 +722,17 @@ export async function getRuntimeVersionObjectAsync({
workflows: Record<ExpoConfigPlatform, Workflow>;
projectDir: string;
env: Env | undefined;
}): Promise<{ platform: UpdatePublishPlatform; runtimeVersion: string }[]> {
}): Promise<
{
platform: UpdatePublishPlatform;
runtimeVersionInfo: RuntimeVersionInfo;
}[]
> {
return await Promise.all(
platforms.map(async platform => {
return {
platform,
runtimeVersion: await getRuntimeVersionForPlatformAsync({
runtimeVersionInfo: await getRuntimeVersionInfoForPlatformAsync({
exp,
platform,
workflow: workflows[platform],
@@ -727,7 +744,7 @@ export async function getRuntimeVersionObjectAsync({
);
}
async function getRuntimeVersionForPlatformAsync({
async function getRuntimeVersionInfoForPlatformAsync({
exp,
platform,
workflow,
@@ -739,29 +756,31 @@ async function getRuntimeVersionForPlatformAsync({
workflow: Workflow;
projectDir: string;
env: Env | undefined;
}): Promise<string> {
}): Promise<{
runtimeVersion: string;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
}> {
if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) {
try {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
const extraArgs = Log.isDebug ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
const runtimeVersionResult = await resolveRuntimeVersionUsingCLIAsync({
platform,
workflow,
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env }
);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
env,
});
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
return nullthrows(
runtimeVersionResult.runtimeVersion,
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
);
return {
...runtimeVersionResult,
runtimeVersion: nullthrows(
runtimeVersionResult.runtimeVersion,
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
};
} catch (e: any) {
// if it's a known set of errors thrown by the CLI it means that we need to default back to the
// previous behavior, otherwise we throw the error since something is wrong
@@ -791,23 +810,37 @@ async function getRuntimeVersionForPlatformAsync({
);
}
return resolvedRuntimeVersion;
return {
runtimeVersion: resolvedRuntimeVersion,
fingerprint: null,
};
}
export function getRuntimeToPlatformMappingFromRuntimeVersions(
runtimeVersions: { platform: UpdatePublishPlatform; runtimeVersion: string }[]
): { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[] {
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[] =
[];
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 });
export function getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
runtimeVersionInfoObjects: {
platform: UpdatePublishPlatform;
runtimeVersionInfo: RuntimeVersionInfo;
}[]
): (RuntimeVersionInfo & { platforms: UpdatePublishPlatform[] })[] {
const groupedRuntimeVersionInfoObjects = groupBy(
runtimeVersionInfoObjects,
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.runtimeVersion
);
return Object.entries(groupedRuntimeVersionInfoObjects).map(
([runtimeVersion, runtimeVersionInfoObjects]) => {
return {
runtimeVersion,
platforms: runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.platform
),
fingerprint:
runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.fingerprint
)[0] ?? null,
};
}
}
return runtimeToPlatformMapping;
);
}
export const platformDisplayNames: Record<UpdatePublishPlatform, string> = {
@@ -841,19 +874,18 @@ export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync(
appId,
branchName,
rolloutPercentage,
runtimeToPlatformMapping,
runtimeToPlatformsAndFingerprintInfoMapping,
}: {
appId: string;
branchName: string;
rolloutPercentage: number;
runtimeToPlatformMapping: { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[];
runtimeToPlatformsAndFingerprintInfoMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
})[];
}
): Promise<Map<string, UpdateRolloutInfoGroup>> {
const runtimeToPlatformsMap = new Map(
runtimeToPlatformMapping.map<[string, UpdatePublishPlatform[]]>(r => [
r.runtimeVersion,
r.platforms,
])
runtimeToPlatformsAndFingerprintInfoMapping.map(r => [r.runtimeVersion, r.platforms])
);
return await mapMapAsync(runtimeToPlatformsMap, async (platforms, runtimeVersion) => {
return Object.fromEntries(
@@ -1,17 +1,32 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { AppVersionSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { BuildFlags } from '../build/types';
import Log, { learnMore } from '../log';
import { confirmAsync, selectAsync } from '../prompts';
import { ProfileData } from '../utils/profiles';
export enum AppVersionSourceUpdateOption {
SET_TO_REMOTE,
SET_TO_LOCAL,
ABORT,
}
export async function ensureVersionSourceIsRemoteAsync(
easJsonAccessor: EasJsonAccessor,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<void> {
const easJsonCliConfig = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
let easJsonCliConfig = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (easJsonCliConfig?.appVersionSource === undefined) {
easJsonCliConfig = await ensureAppVersionSourceIsSetAsync(
easJsonAccessor,
easJsonCliConfig ?? undefined,
nonInteractive
);
}
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
return;
}
@@ -45,10 +60,31 @@ export async function ensureVersionSourceIsRemoteAsync(
Log.withTick('Updated eas.json');
}
export function validateBuildProfileVersionSettings(
export async function validateBuildProfileVersionSettingsAsync(
profileInfo: ProfileData<'build'>,
cliConfig: EasJson['cli']
): void {
cliConfig: EasJson['cli'],
projectDir: string,
flags: BuildFlags
): Promise<void> {
if (
cliConfig?.appVersionSource === undefined &&
profileInfo.profile.autoIncrement !== 'version'
) {
if (profileInfo.profile.autoIncrement !== true) {
Log.warn(
`The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
} else {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
cliConfig = await ensureAppVersionSourceIsSetAsync(
easJsonAccessor,
cliConfig,
flags.nonInteractive
);
}
}
if (cliConfig?.appVersionSource !== AppVersionSource.REMOTE) {
return;
}
@@ -98,3 +134,96 @@ export function getBuildVersionName(platform: Platform): string {
return 'buildNumber';
}
}
export async function ensureAppVersionSourceIsSetAsync(
easJsonAccessor: EasJsonAccessor,
easJsonCliConfig: EasJson['cli'] | undefined,
nonInteractive: boolean
): Promise<EasJson['cli'] | undefined> {
let selectOption, updateEasJson;
if (nonInteractive) {
Log.warn(
`The field "cli.appVersionSource" is not set, but it will be required in the future Proceeding with the default "local" value. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
selectOption = AppVersionSourceUpdateOption.SET_TO_LOCAL;
updateEasJson = false;
} else {
Log.log(
'Since EAS CLI version `12.0.0` explicitly specifying app version source is required. Please select your app version source:'
);
Log.log(
`\t1) With the "local" app version source and "autoIncrement" option enabled, the build number/version code is sourced from local project files and incremented automatically if possible, by editing local project files. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/#local-version-source'
)}`
);
Log.log(
`\t2) With the "remote" app version source and "autoIncrement" option enabled, the build number/version code is stored on EAS servers and updated every time you create a new build. Remote auto-incrementation won't edit the version in the local project files, but instead, the new version will be injected automatically during the build process. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/#remote-version-source'
)}`
);
Log.log(
`Until now, this project has been using the "local" version source (which was the previous default). App version source can now be set for you automatically, or you can configure it manually by setting the "appVersionSource" value in your eas.json.`
);
selectOption = await selectAsync(`What would you like to do?`, [
{
title: 'Update eas.json to use the default "remote" version source (recommended)',
value: AppVersionSourceUpdateOption.SET_TO_REMOTE,
},
{
title: 'Update eas.json to use "local" version source (old behavior)',
value: AppVersionSourceUpdateOption.SET_TO_LOCAL,
},
{
title: "Don't update eas.json, abort command and configure manually",
value: AppVersionSourceUpdateOption.ABORT,
},
]);
updateEasJson = true;
}
if (selectOption === AppVersionSourceUpdateOption.SET_TO_LOCAL) {
if (updateEasJson) {
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = {
...easJsonRawObject?.cli,
appVersionSource: AppVersionSource.LOCAL,
};
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
}
if (easJsonCliConfig) {
easJsonCliConfig.appVersionSource = AppVersionSource.LOCAL;
}
Log.withTick('Updated eas.json');
} else if (selectOption === AppVersionSourceUpdateOption.SET_TO_REMOTE) {
if (updateEasJson) {
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = {
...easJsonRawObject?.cli,
appVersionSource: AppVersionSource.REMOTE,
};
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
}
if (easJsonCliConfig) {
easJsonCliConfig.appVersionSource = AppVersionSource.REMOTE;
}
Log.withTick('Updated eas.json');
} else {
Log.warn(
`You'll need to configure ${chalk.bold('appVersionSource')} manually. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
Errors.error('Aborted.', { exit: 1 });
}
return easJsonCliConfig;
}
@@ -9,6 +9,52 @@ import {
expoUpdatesCommandAsync,
} from '../utils/expoUpdatesCli';
export async function resolveRuntimeVersionUsingCLIAsync({
platform,
workflow,
projectDir,
env,
cwd,
}: {
platform: 'ios' | 'android';
workflow: Workflow;
projectDir: string;
env: Env | undefined;
cwd?: string;
}): Promise<{
runtimeVersion: string | null;
fingerprint: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
}> {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
const useDebugFingerprintSource = Log.isDebug;
const extraArgs = useDebugFingerprintSource ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env, cwd }
);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
return {
runtimeVersion: runtimeVersionResult.runtimeVersion ?? null,
fingerprint: runtimeVersionResult.fingerprintSources
? {
fingerprintSources: runtimeVersionResult.fingerprintSources,
isDebugFingerprintSource: useDebugFingerprintSource,
}
: null,
};
}
export async function resolveRuntimeVersionAsync({
exp,
platform,
@@ -40,31 +86,7 @@ export async function resolveRuntimeVersionAsync({
}
try {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
const useDebugFingerprintSource = Log.isDebug;
const extraArgs = useDebugFingerprintSource ? ['--debug'] : [];
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
projectDir,
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
{ env, cwd }
);
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
Log.debug('runtimeversion:resolve output:');
Log.debug(resolvedRuntimeVersionJSONResult);
return {
runtimeVersion: runtimeVersionResult.runtimeVersion ?? null,
fingerprint: runtimeVersionResult.fingerprintSources
? {
fingerprintSources: runtimeVersionResult.fingerprintSources,
isDebugFingerprintSource: useDebugFingerprintSource,
}
: null,
};
return await resolveRuntimeVersionUsingCLIAsync({ platform, workflow, projectDir, env, cwd });
} catch (e: any) {
// if expo-updates is not installed, there's no need for a runtime version in the build
if (e instanceof ExpoUpdatesCLIModuleNotFoundError) {
+17 -1
View File
@@ -7,7 +7,7 @@ import * as uuid from 'uuid';
import { getRecentBuildsForSubmissionAsync } from './utils/builds';
import { isExistingFileAsync, uploadAppArchiveAsync } from './utils/files';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { BuildFragment } from '../graphql/generated';
import { BuildFragment, BuildStatus } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log, { learnMore } from '../log';
@@ -89,6 +89,16 @@ export type ArchiveSource =
export type ResolvedArchiveSource = ArchiveUrlSource | ArchiveGCSSource | ArchiveBuildSource;
const buildStatusMapping: Record<BuildStatus, string> = {
[BuildStatus.New]: 'new',
[BuildStatus.InQueue]: 'in queue',
[BuildStatus.InProgress]: 'in progress',
[BuildStatus.Finished]: 'finished',
[BuildStatus.Errored]: 'errored',
[BuildStatus.PendingCancel]: 'canceled',
[BuildStatus.Canceled]: 'canceled',
};
export async function getArchiveAsync(
ctx: ArchiveResolverContext,
source: ArchiveSource
@@ -344,6 +354,7 @@ function formatBuildChoice(build: BuildFragment): prompts.Choice {
gitCommitMessage,
channel,
message,
status,
} = build;
const buildDate = new Date(updatedAt);
@@ -368,6 +379,7 @@ function formatBuildChoice(build: BuildFragment): prompts.Choice {
? chalk.bold(message.length > 200 ? `${message.slice(0, 200)}...` : message)
: null,
},
{ name: 'Status', value: buildStatusMapping[status] },
];
const filteredDescriptionArray: string[] = descriptionItems
@@ -385,6 +397,10 @@ function formatBuildChoice(build: BuildFragment): prompts.Choice {
async function handlePromptSourceAsync(
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
if (ctx.nonInteractive) {
throw new Error('Please run this command with appropriate input.');
}
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
type: 'select',
+6 -2
View File
@@ -65,9 +65,13 @@ export default abstract class BaseSubmitter<
return resolvedSourceOptions;
}
public async submitAsync(): Promise<SubmissionFragment> {
public async getSubmissionInputAsync(): Promise<SubmissionInput<P>> {
const resolvedSourceOptions = await this.getSourceOptionsAsync();
const input = await this.createSubmissionInputAsync(resolvedSourceOptions);
return await this.createSubmissionInputAsync(resolvedSourceOptions);
}
public async submitAsync(): Promise<SubmissionFragment> {
const input = await this.getSubmissionInputAsync();
return await this.createSubmissionWithAnalyticsAsync(input);
}
@@ -7,6 +7,7 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import {
AppPlatform,
BuildFragment,
BuildStatus,
SubmissionArchiveSourceType,
UploadSessionType,
} from '../../graphql/generated';
@@ -41,6 +42,17 @@ const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
status: BuildStatus.Finished,
};
const MOCK_IN_PROGRESS_BUILD_FRAGMENT: Partial<BuildFragment> = {
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
status: BuildStatus.InProgress,
};
const SOURCE_STUB_INPUT = {
@@ -246,7 +258,7 @@ describe(getArchiveAsync, () => {
expect(archive.sourceType).toBe(ArchiveSourceType.url);
});
it('handles build-list-select source', async () => {
it('handles build-list-select source for finished builds', async () => {
const projectId = uuidv4();
jest
.mocked(getRecentBuildsForSubmissionAsync)
@@ -269,6 +281,57 @@ describe(getArchiveAsync, () => {
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('handles build-list-select source for in-progress builds', async () => {
const projectId = uuidv4();
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([MOCK_IN_PROGRESS_BUILD_FRAGMENT as BuildFragment]);
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ selectedBuild: MOCK_IN_PROGRESS_BUILD_FRAGMENT });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.buildList,
}
);
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('handles build-list-select source for both finished and in-progress builds', async () => {
const projectId = uuidv4();
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([
MOCK_IN_PROGRESS_BUILD_FRAGMENT as BuildFragment,
MOCK_BUILD_FRAGMENT as BuildFragment,
]);
jest.mocked(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.buildList,
}
);
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
});
it('prompts again if all builds have expired', async () => {
jest.mocked(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([
{
@@ -4,11 +4,7 @@ import { Result, result } from '@expo/results';
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
import {
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
SubmissionFragment,
} from '../../graphql/generated';
import { SubmissionAndroidReleaseStatus, SubmissionAndroidTrack } from '../../graphql/generated';
import Log from '../../log';
import {
AmbiguousApplicationIdError,
@@ -22,7 +18,7 @@ import { SubmissionContext } from '../context';
export default class AndroidSubmitCommand {
constructor(private ctx: SubmissionContext<Platform.ANDROID>) {}
async runAsync(): Promise<SubmissionFragment> {
async runAsync(): Promise<AndroidSubmitter> {
Log.addNewLineIfNone();
const archiveSource = this.resolveArchiveSource();
if (!archiveSource.ok) {
@@ -48,7 +44,7 @@ export default class AndroidSubmitCommand {
}
const submissionOptions = await this.getAndroidSubmissionOptionsAsync(archiveSourceValue);
const submitter = new AndroidSubmitter(this.ctx, submissionOptions, archive);
return await submitter.submitAsync();
return submitter;
}
private async getAndroidSubmissionOptionsAsync(
@@ -132,7 +132,9 @@ describe(AndroidSubmitCommand, () => {
vcsClient,
});
const command = new AndroidSubmitCommand(ctx);
await expect(command.runAsync()).rejects.toThrowError(
await expect(
command.runAsync().then(submitter => submitter.submitAsync())
).rejects.toThrowError(
'Google Service Account Keys cannot be set up in --non-interactive mode.'
);
});
@@ -170,7 +172,8 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -216,7 +219,8 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -264,7 +268,8 @@ describe(AndroidSubmitCommand, () => {
vcsClient,
});
const command = new AndroidSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -329,7 +334,8 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(
graphqlClient,
@@ -391,7 +397,8 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(
graphqlClient,
@@ -461,7 +468,8 @@ describe(AndroidSubmitCommand, () => {
});
const command = new AndroidSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(
graphqlClient,
@@ -12,7 +12,6 @@ import {
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
import { MissingCredentialsError } from '../../credentials/errors';
import { SubmissionFragment } from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { ArchiveSource, ArchiveSourceType, getArchiveAsync } from '../ArchiveSource';
import { refreshContextSubmitProfileAsync, resolveArchiveSource } from '../commons';
@@ -21,7 +20,7 @@ import { SubmissionContext } from '../context';
export default class IosSubmitCommand {
constructor(private ctx: SubmissionContext<Platform.IOS>) {}
async runAsync(): Promise<SubmissionFragment> {
async runAsync(): Promise<IosSubmitter> {
Log.addNewLineIfNone();
const archiveSource = this.resolveArchiveSource();
if (!archiveSource.ok) {
@@ -47,7 +46,7 @@ export default class IosSubmitCommand {
}
const options = await this.resolveSubmissionOptionsAsync(archiveSourceValue);
const submitter = new IosSubmitter(this.ctx, options, archive);
return await submitter.submitAsync();
return submitter;
}
private async resolveSubmissionOptionsAsync(
@@ -109,7 +109,7 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await expect(command.runAsync()).rejects.toThrow(
await expect(command.runAsync().then(submitter => submitter.submitAsync())).rejects.toThrow(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format xxxx-xxxx-xxxx-xxxx, where x is a lowercase letter.'
);
@@ -144,7 +144,9 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await expect(command.runAsync()).rejects.toThrowError();
await expect(
command.runAsync().then(submitter => submitter.submitAsync())
).rejects.toThrowError();
});
});
@@ -180,7 +182,8 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -245,7 +248,8 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -304,7 +308,8 @@ describe(IosSubmitCommand, () => {
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
@@ -369,7 +374,8 @@ describe(IosSubmitCommand, () => {
specifiedProfile: 'specificProfile',
});
const command = new IosSubmitCommand(ctx);
await command.runAsync();
const submitter = await command.runAsync();
await submitter.submitAsync();
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
+2 -1
View File
@@ -23,7 +23,8 @@ export async function submitAsync<T extends Platform>(
ctx.platform === Platform.ANDROID
? new AndroidSubmitCommand(ctx as SubmissionContext<Platform.ANDROID>)
: new IosSubmitCommand(ctx as SubmissionContext<Platform.IOS>);
return await command.runAsync();
const submitter = await command.runAsync();
return await submitter.submitAsync();
},
{
attemptEvent: SubmissionEvent.SUBMIT_COMMAND_ATTEMPT,
@@ -0,0 +1,226 @@
import { v4 as uuidv4 } from 'uuid';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
AppPlatform,
BuildFragment,
BuildStatus,
SubmissionArchiveSourceType,
} from '../../../graphql/generated';
import { BuildQuery } from '../../../graphql/queries/BuildQuery';
import { getRecentBuildsForSubmissionAsync } from '../builds';
jest.mock('../../../graphql/queries/BuildQuery', () => ({
BuildQuery: {
viewBuildsOnAppAsync: jest.fn(),
},
}));
const ARCHIVE_SOURCE = {
type: SubmissionArchiveSourceType.Url,
url: 'https://url.to/archive.tar.gz',
};
const MOCK_BUILD_FRAGMENTS: Partial<BuildFragment>[] = Array(5).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.Finished,
}));
const MOCK_IN_PROGRESS_BUILD_FRAGMENTS: Partial<BuildFragment>[] = Array(2).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.InProgress,
}));
const MOCK_IN_QUEUE_BUILD_FRAGMENTS = Array(2).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.InQueue,
}));
const MOCK_NEW_BUILD_FRAGMENTS = Array(1).map(() => ({
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
updatedAt: Date.now(),
createdAt: Date.now(),
status: BuildStatus.New,
}));
describe(getRecentBuildsForSubmissionAsync, () => {
let graphqlClient: ExpoGraphqlClient;
beforeEach(() => {
graphqlClient = {} as any as ExpoGraphqlClient;
});
it('returns finished builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce(
MOCK_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_BUILD_FRAGMENTS.length)
) as BuildFragment[]
);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_BUILD_FRAGMENTS.slice(0, Math.min(limit, MOCK_BUILD_FRAGMENTS.length))
);
});
it('returns in-progress builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce(
MOCK_IN_PROGRESS_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_PROGRESS_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce([] as BuildFragment[]);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_IN_PROGRESS_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_PROGRESS_BUILD_FRAGMENTS.length)
)
);
});
it('returns in-queue builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce(
MOCK_IN_QUEUE_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_QUEUE_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[]);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_IN_QUEUE_BUILD_FRAGMENTS.slice(0, Math.min(limit, MOCK_IN_QUEUE_BUILD_FRAGMENTS.length))
);
});
it('returns new builds', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce(
MOCK_NEW_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_NEW_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[])
.mockResolvedValueOnce([] as BuildFragment[]);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject(
MOCK_NEW_BUILD_FRAGMENTS.slice(0, Math.min(limit, MOCK_NEW_BUILD_FRAGMENTS.length))
);
});
it('returns up to "limit" newest builds regardless of status', async () => {
const appId = uuidv4();
const limit = 2;
jest
.mocked(BuildQuery.viewBuildsOnAppAsync)
.mockResolvedValueOnce(
MOCK_NEW_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_NEW_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce(
MOCK_IN_QUEUE_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_QUEUE_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce(
MOCK_IN_PROGRESS_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_IN_PROGRESS_BUILD_FRAGMENTS.length)
) as BuildFragment[]
)
.mockResolvedValueOnce(
MOCK_BUILD_FRAGMENTS.slice(
0,
Math.min(limit, MOCK_BUILD_FRAGMENTS.length)
) as BuildFragment[]
);
const result = await getRecentBuildsForSubmissionAsync(
graphqlClient,
AppPlatform.Android,
appId,
{ limit }
);
expect(result).toMatchObject([MOCK_NEW_BUILD_FRAGMENTS[0], MOCK_IN_QUEUE_BUILD_FRAGMENTS[1]]);
});
});
+25 -10
View File
@@ -8,14 +8,29 @@ export async function getRecentBuildsForSubmissionAsync(
appId: string,
{ limit = 1 }: { limit?: number } = {}
): Promise<BuildFragment[]> {
return await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId,
limit,
offset: 0,
filter: {
platform,
distribution: DistributionType.Store,
status: BuildStatus.Finished,
},
});
const allowedStatuses = [
BuildStatus.New,
BuildStatus.InQueue,
BuildStatus.InProgress,
BuildStatus.Finished,
];
const buildsPromises: Promise<BuildFragment[]>[] = [];
for (const buildStatus of allowedStatuses) {
buildsPromises.push(
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId,
limit,
offset: 0,
filter: {
platform,
distribution: DistributionType.Store,
status: buildStatus,
},
})
);
}
const builds = (await Promise.all(buildsPromises)).reduce((acc, value) => acc.concat(value), []);
builds.sort((buildA, buildB) => (buildA.createdAt > buildB.createdAt ? -1 : 1));
return builds.slice(0, limit);
}
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more