Compare commits

...
Author SHA1 Message Date
Kadi Kraman 38e64cbd9a v16.17.0 2025-07-18 18:14:57 +01:00
Kadi Kraman 2cd7194ef6 Fix formatting in changelog 2025-07-18 17:58:55 +01:00
Kadi Kraman 6c33c8a656 Fix changelog 2025-07-18 17:56:27 +01:00
Kadi Kraman 92643026f3 [eas-cli] Add command for deleting worker aliases (#3107) 2025-07-18 16:28:53 +01:00
Kadi Kraman b8a056b43e [eas-cli] Add command for deleting worker deployments (#3106) 2025-07-18 12:56:09 +01:00
Vojtech Novak 1ba5cb07ce [eas-cli] change push notification confirmation message (#3077) 2025-07-18 10:56:28 +02:00
Phil Pluckthun 8e96143b59 v16.16.0 2025-07-16 23:35:52 +01:00
Phil Pluckthun 52512c333d [eas-cli] Fix batch uploads being subject to early-termination issues corrupting multipart upload parts (#3103)
* Workaround early stream termination using filehandle.read

* Add length check to createReadStreamAsync

* Align asset and multipart entry file types

* Recalculate and check hash during upload

* Update error messages and add comment

* Emit errors earlier and refactor loop

* Use same reader function and disable objectMode

* Add CHANGELOG entry

* Apply lints
2025-07-16 14:34:10 +01:00
Douglas Lowder 576c798666 New commands workflow:view and workflow:logs (#3090)
# Why

Adding the new `workflow:logs` command (see the [workflow CLI design document](https://www.notion.so/expo/EAS-CLI-workflow-commands-8bc5ccc4f9f949c8adfec1347817113c)) and the related command `workflow:view`, to provide more details on workflow runs, jobs, and their associated logs.

```bash
$ eas workflow:view --help
view details for a workflow run, including jobs. If no run ID is provided, you will be prompted to select
from recent workflow runs for the current project.

USAGE
  $ eas workflow:view [ID] [--json] [--non-interactive]

ARGUMENTS
  ID  ID of the workflow run to view

FLAGS
  --json             Enable JSON output, non-JSON messages will be printed to stderr.
  --non-interactive  Run the command in non-interactive mode.

DESCRIPTION
  view details for a workflow run, including jobs. If no run ID is provided, you will be prompted to select
from recent
  workflow runs for the current project.
```
```bash
$ eas workflow:logs --help
view logs for a workflow run, selecting a job and step to view. You can pass in either a workflow run ID or a job ID. 
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.

USAGE
  $ eas workflow:logs [ID] [--json] [--non-interactive] [--allSteps]

ARGUMENTS
  ID  ID of the workflow run or workflow job to view logs for

FLAGS
  --allSteps         Print all logs, rather than prompting for a specific step. This will be automatically set when in
                     non-interactive mode.
  --json             Enable JSON output, non-JSON messages will be printed to stderr.
  --non-interactive  Run the command in non-interactive mode.

DESCRIPTION
  view logs for a workflow run, selecting a job and step to view. You can pass in either a workflow run ID or a job ID.
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
```
# How

Implemented the new commands. New queries and and a new fragment for workflow jobs have been added.

A state machine has been implemented to ensure correct flow for the `workflow:logs` command, allowing the user to drill down from workflow runs, to jobs, to viewing logs for an individual step. Where it makes sense, the selection prompts allow backtracking to search for a different run, job, etc.

# Test Plan

- CI should pass
- Tested locally with different inputs, and with different flag values
- New unit tests added for `workflow:logs`
2025-07-15 21:19:01 -07:00
Vojtech Novak 356d6267ec [eas-cli] fix CapabilityModel creation and syncing (#3088)
# Why

The current implementation of capabilities syncing can lead to provisioning profiles becoming invalid. This PR refactors the syncing process to avoid this issue by making the following improvements:

1. Preventing unnecessary creation of capability identifiers that already exist
2. Only updating bundle capabilities when necessary

closes https://github.com/expo/eas-cli/issues/1069
closes https://github.com/expo/eas-cli/issues/1445

# How

I refactored the capability syncing logic to:

- Add test coverage for faulty behaviors. More tests are [up the stack](https://github.com/expo/eas-cli/pull/3086/files#diff-09c5aad6e72a7a336e9783710d3899ff2ea432146929bc07ba8bfec7c7bbd2a8).
- Only create capability identifiers that don't already exist on the server
- Skip updating bundle capabilities when no changes are needed


# Test Plan

`easd` command below is from https://github.com/expo/eas-cli/blob/main/CONTRIBUTING.md#development.

To best test this locally, have an iOS app with 

(1) no capabilities enabled. Enable some capabilities in `app.json` and run prebuild. [iCloud](https://docs.expo.dev/versions/latest/sdk/document-picker/#example-appjson-with-config-plugin), Apple Sign In, app groups, or `"com.apple.developer.default-data-protection": "NSFileProtectionComplete / ..."` are of special interest, but any will do.

OR

(2) a buch of capabilities already enabled

using this app, run `easd build -p ios` TWO times. Confirm that the first build creates new capabilities on your app bundle (https://developer.apple.com/account/resources/identifiers/list) and provisioning profiles (if any). Confirm the second build passes and doesn't require any provisioning profile changes.

Finally, change the capabilities, and again run `easd build -p ios` twice to confirm no capabilities / provisioning profiles are changed on the second run (that's what should happen on the first run).

- green CI
- local tests
2025-07-14 22:34:47 +02:00
Vojtech Novak a88897b40e [eas-cli] refactor capability syncing (#3087)
# Why

This PR refactors the iOS capability synchronization logic to make it more "linear" and easier to maintain

# How

- Encapsulating capability-specific logic with the capability definition
- Making the operation decision process more explicit and easier to follow

# Test Plan

- green CI
- local tests
2025-07-14 22:29:17 +02:00
Vojtech Novak 8431167a13 [eas-cli] fix capability syncing (#3086)
# Why

This PR refactors the iOS bundle ID capabilities management to improve handling of capabilities with complex settings. The current implementation doesn't properly handle capabilities that have settings - Sign In with Apple, iCloud, and Data Protection.


# How

- Added test cases that would previously fail
- Extracted capability list to a separate file (`capabilityList.ts`) to improve code organization
- Fixed synchronization logic to make tests pass (in a dirty way, refactored later)
- Fixed handling of MDM Managed Associated Domains to prevent disabling when Associated Domains is enabled
- Improved test assertions to use non-deprecated matchers (`toHaveBeenCalledWith` instead of `toBeCalled`)

# Test Plan

- green CI
- local testing
2025-07-14 22:09:24 +02:00
Stanisław Chmiela d87f8d5750 [eas-cli] Add support for workflow:run inputs (#3095)
<!-- 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 want to allow users to provide inputs to workflow runs.

# How

Added support for providing inputs with `-F` flag. It follows [`gh-cli`'s example](https://docs.github.com/en/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow#running-a-workflow).

<img width="748" alt="Zrzut ekranu 2025-07-7 o 14 44 09" src="https://github.com/user-attachments/assets/d2ae7fda-5ad8-44c2-9873-d396b02e4f17" />

I am explicitly not supporting `-f` because the backend does not allow "raw field with no type coercion". It always tries to resolve from whatever is the input to the right type.

# Test Plan

Ran `easd workflow:run upload-download-test.yml -F name=test -F test=test2` and the workflow run defined two inputs: `name` and `test`.

- [workflow run](https://expo.dev/accounts/sjchmiela/projects/test-production/workflows/0197f5ea-a397-734f-81e0-8a5e78e3aa90) from `easd workflow:run inputs-test.yml`
- [workflow run](https://expo.dev/accounts/sjchmiela/projects/test-production/workflows/0197f5f0-7206-73e2-b741-409decc6e9ca) from `easd workflow:run inputs-test.yml -F name=Doug`
2025-07-14 20:41:04 +02:00
Douglas Lowder 1a261cf043 Add GraphQL check to release workflow (#3101)
# Why

Noticed that a recent release failed the GraphQL check. This ensures that the release will fail if the check does not pass.
2025-07-11 12:35:16 -07:00
Expo CI 9daa933990 update CHANGELOG.md 2025-07-10 17:37:51 +00:00
Phil Pluckthun 72f3ba817f v16.15.0 2025-07-10 18:30:29 +01:00
Phil Pluckthun e8ffdf25f7 feat(eas-cli): Implement new batched EAS Hosting asset upload protocol (#3085)
* Add custom multipart format

* Reuse list of files for upload params

* Remove unused gzip compression

* Clean up UploadParams

* Update file crawling to include size and mime type

* Move file-size check to assets.ts

* Refactor upload functions

* Update upload API function to support multipart uploads

* Update uploadAsync params usage in deploy command

* Upload as multipart batches, if instructed

* Count batch length against total

* Add CHANGELOG

* Fix signal not being used

* Add new assets.json format

* Skip empty assets

* Fix lint error

* Adjust concurrency

Beyond 10 we do risk some CPU contention in rare cases for small files
(especially on Windows)
However, we can now run more requests concurrently, since they're slower
each and hence are less likely to run into network issues due to lower
rate.

* Revert "Skip empty assets"

This reverts commit 18434cdb48.

* feat(eas-cli): Implement new asset upload progress bar for `deploy` command (#3099)

* Update comment

* Add CHANGELOG entry for progress bar change
2025-07-10 18:25:45 +01:00
Quinlan Jung 196abc9a9e fix changelog 2025-07-08 12:59:11 -07:00
Expo CI 037529689a update CHANGELOG.md 2025-07-08 19:15:28 +00:00
Quinlan Jung 2502b843f8 v16.14.1 2025-07-08 12:09:44 -07:00
Quinlan Jung 🦥 012e75f8ee [eas-cli] update apple team info as needed (#3096)
* [eas-cli] update apple team info as needed

* Temporary Commit at 7/7/2025, 2:01:08 PM

* Temporary Commit at 7/7/2025, 2:12:28 PM

* Temporary Commit at 7/8/2025, 11:28:08 AM
2025-07-08 12:08:34 -07:00
Expo CI 5ada92430b update CHANGELOG.md 2025-07-07 22:52:34 +00:00
Will Schurman bf27ec5238 v16.14.0 2025-07-07 15:47:08 -07:00
Will Schurman 🦦 8f18b35fe8 [eas-cli] Fix graphql codegen to look in all directories (#3097)
<!-- 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

While creating a previous PR, I moved a graphql query into a directory not explicitly specified in `graphql-codegen.yml`. It took me a while to figure out what was going on (only error was `tsc`).

To avoid other devs running into this, just generate it for all files in src.

# How

1. Update paths
2. Fix `fetchUserAsync` to be typed and have a unique query name.

# Test Plan

```
neas logout
neas login
neas whoami
```
2025-07-07 14:24:25 -07:00
Will Schurman 🦦 e82b8966b0 [eas-cli] Add update:revert-update-rollout command (#3068)
<!-- 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

The docs for ending an update-based rollout are incorrect: https://docs.expo.dev/eas-update/rollouts/#ending-a-rollout

In order to revert a update-based rollout, it currently says:
> To accomplish this end state, republish the previous update by using the `eas update:republish` command.

But this is incorrect since one is not allowed to publish a new update until the control update is finished (can't have multiple roll-outs).

What actually needs to happen under the hood is one of two options:

- When on a branch with a rollout with a control update
    1. Delete the rollout update
    2. Republish the control update
- When on a branch with a rollout with only a rollout update
    1. Delete the rollout update
    2. Publish a roll-back-to-embedded (after warning since this may not be intended behavior).

This PR adds a new command, `eas update:revert-update-rollout` that does this.

# How

Add command, refactoring shared code where necessary.

This handles a ton of cases. Essentially the cross product of:
- Rollout published on empty branch or branch with existing updates. (rollout has control)
- Rollout published per-platform or single group with both platforms,
- Control published per-platform or single group with both platforms.

So, 8 total cases.

# Test Plan

Tested most of the 8 cases.

Normal update group rollout (both platforms), normal control on same update group:

```
(branch with an existing notmal update group)
$ neas update --rollout-percentage=50
$ neas update:revert-update-rollout
✔ Find update by branch or channel? › Branch
✔ Select branch from which to choose update › main
✔ Select a target runtime › 1.0.0
✔ Are you sure you want to revert the rollout update group with ID "b0db8e15-d3d0-41db-985a-d74f0bd8fb00"? This will delete the rollout update group and republish the control update group (ID: "7df732c7-ccad-4755-8b80-acd1c03c5a81"). … yes
✔ Provide an update message. … watwatrep
✔ Republished update group
```

Normal update group rollout (both platforms), controls in different update groups (latest updates for each platform at time of rollout publish were in different groups):
```
$ neas update --platform android
$ neas update --platform ios
$ neas update --rollout-percentage=50
$ neas update:revert-update-rollout
✔ Find update by branch or channel? › Branch
✔ Select branch from which to choose update › main
✔ Select a target runtime › 1.0.0
✔ Are you sure you want to revert the rollout update group with ID "47ef8fe7-6f9b-4c59-bdd5-cdcc12eef0f2"? This will delete the rollout update group and republish the control update groups (IDs: "a821e0be-04af-455b-8b75-ceabea3afc09", "5a07a994-7a71-4d4e-a188-83df9e456e2c"). … yes
✔ Provide an update message. … Republish "ioswat" - group: a821e0be-04af-455b-8b75-ceabea3afc09
✔ Provide an update message. … Republish "watandroid" - group: 5a07a994-7a71-4d4e-a188-83df9e456e2c
✔ Republished update group

Branch           main
Runtime version  1.0.0
Platform         ios
Update group ID  c1d05402-4391-4cd8-9eb0-ab0c7958bce5
iOS update ID    06b57ea9-4016-40b7-962b-756d463f2ba9
Message          Republish "ioswat" - group: a821e0be-04af-455b-8b75-ceabea3afc09
EAS Dashboard    https://staging.expo.dev/accounts/wschurman/projects/test-ojdasksajajaja/updates/c1d05402-4391-4cd8-9eb0-ab0c7958bce5
✔ Republished update group

Branch             main
Runtime version    1.0.0
Platform           android
Update group ID    56d10192-9bbd-4557-ab25-44250b3ccf6b
Android update ID  eb5b1a25-cbba-4f2a-afe9-d698068369ed
Message            Republish "watandroid" - group: 5a07a994-7a71-4d4e-a188-83df9e456e2c
EAS Dashboard      https://staging.expo.dev/accounts/wschurman/projects/test-ojdasksajajaja/updates/56d10192-9bbd-4557-ab25-44250b3ccf6b
```

Two single platform rollouts:
```
$ neas update --platform android
$ neas update --platform ios
$ neas update --rollout-percentage=50 --platform android
$ neas update --rollout-percentage=50 --platform ios
$ neas update:revert-update-rollout
✔ Find update by branch or channel? › Branch
✔ Select branch from which to choose update › main
✔ Select a target runtime › 1.0.0
✔ Which rollout update group would you like to revert? › Rollout update group ID: 38369dcd-fe24-40c5-a85d-c7d4c03c9230, Platform: android, Rollout Percentage: 50
✔ Are you sure you want to revert the rollout update group with ID "38369dcd-fe24-40c5-a85d-c7d4c03c9230"? This will delete the rollout update group and republish the control update group (ID: "5a07a994-7a71-4d4e-a188-83df9e456e2c"). … yes
✔ Provide an update message. … Republish "watandroid" - group: 5a07a994-7a71-4d4e-a188-83df9e456e2c
✔ Republished update group

$ neas update:revert-update-rollout
(node:96817) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
✔ Find update by branch or channel? › Branch
✔ Select branch from which to choose update › main
✔ Select a target runtime › 1.0.0
✔ Are you sure you want to revert the rollout update group with ID "ae7aaa74-06cc-43b1-98dd-63e019cb4a64"? This will delete the rollout update group and republish the control update group (ID: "a821e0be-04af-455b-8b75-ceabea3afc09"). … yes
✔ Provide an update message. … Republish "ioswat" - group: a821e0be-04af-455b-8b75-ceabea3afc09
✔ Republished update group
```

Empty branch rollout (both platforms), no control:

```
(empty branch)
$ neas update --rollout-percentage=50
$ neas update:revert-update-rollout
✔ Find update by branch or channel? › Branch
✔ Select branch from which to choose update › main
✔ Select a target runtime › 1.0.0
✔ Are you sure you want to revert the rollout update group with ID "1797d1fe-cf58-4134-abe1-b0333250da55"? This will delete the rollout update group and publish a new roll-back-to-embedded update (no control update to roll back to), whose behavior may not be a true revert depending on the previous state of the branch. … yes
✔ Provide an update message: … geageawga
✔ Published!
```

Empty branch rollout (just one platform), no control:
```
(empty branch)
$ neas update --rollout-percentage=50 --platform android
$ neas update:revert-update-rollout
(node:87664) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
✔ Find update by branch or channel? › Branch
✔ Select branch from which to choose update › main
✔ Select a target runtime › 1.0.0
✔ Are you sure you want to revert the rollout update group with ID "b2115695-ba9f-4f2d-a901-9f147d09e113"? This will delete the rollout update group and publish a new roll-back-to-embedded update (no control update to roll back to), whose behavior may not be a true revert depending on the previous state of the branch. … yes
✔ Provide an update message: … dwada
✔ Published!

Branch             main
Runtime version    1.0.0
Platform           android
```
2025-07-07 13:58:09 -07:00
Douglas Lowder fad19a1e03 Workflow to test and verify GraphQL (#3093)
# Why

Now that #3091 is resolved, we need to add some CI to guard against similar future issues with GraphQL breakage.

# How

Add a new workflow that

- Executes GraphQL generation, and fails if there is an error.
- Verifies that for a given PR, any changes in GraphQL generated code or schema files have been checked in as part of the PR. If not, the workflow fails.

The new workflow will execute on pushes, and on a daily schedule to guard against breakage due to changes on the server side.

# Test Plan

- CI should pass
- Push a source change in the GraphQL code, but do not regenerate GraphQL, and verify that the new workflow fails
2025-07-07 12:27:32 -07:00
Kadi Kraman 553d648e86 [eas-cli] Remove link and unlink commands (#3092) 2025-07-06 10:30:56 +01:00
Expo CI 5588a7343d update CHANGELOG.md 2025-07-04 20:32:59 +00:00
Gabriel Donadel 8ec3c52be8 v16.13.4 2025-07-04 17:27:19 -03:00
Gabriel Donadel Dall'Agnol e29d7916b8 [eas-cli] remove hidden flag from upload (#3083)
* [eas-cli] remove hidden flag from upload

* Add changelog entry
2025-07-04 21:39:17 +02:00
Expo CI b9840556fc update CHANGELOG.md 2025-07-01 02:39:04 +00:00
Brent Vatne 3cfb6725e7 v16.13.3 2025-06-30 19:33:34 -07:00
Brent Vatne de794c37f4 [eas-cli] no warning in update:configure when no build profiles (#3084) 2025-06-30 19:09:35 -07:00
James Ide c9a0747762 [build] Replace mention of specific plans (#3082)
Why
===
The message about queuing builds faster mentions the Production and Enterprise plans. The Starter plan is also a good choice.

How
===
This PR changes the message just to say "paid plan" which is simple and understandable.

Test Plan
===
Just automated tests since this is a copy change.
2025-06-30 15:55:32 -07:00
Rolf Bjaanes 80dfcdbe29 [eas-cli] Update systemRequirements.ts with correct fix for missing Simulator (#3079)
<!-- 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

The previously recommended xcode-select command which was `sudo xcode-select -s /Applications/Xcode.app` did not work. Updated to suggest the correct fix which is running `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`


# How

Attempted to use `eas build:dev` after installing Xcode beta to use iOS 26, got error message which suggested the `sudo xcode-select -s /Applications/Xcode.app` command. It didn't work, but `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer` did.


# Test Plan

Not quite sure how to reproduce the scenario in order to test this, but I can verify that this is what the correct suggestion should be as it fixed the problem which triggered the error message, and that's the location where it should look for the Simulator.
2025-06-30 22:25:28 +02:00
Expo CI 6b05dd6226 update CHANGELOG.md 2025-06-27 23:27:36 +00:00
Quinlan Jung f1f189b3f4 v16.13.2 2025-06-27 16:22:34 -07:00
Quinlan Jung 🦥 e9f03f7941 [eas-json] amend update override fields (#3080)
* [eas-json] amend update override fields

* Temporary Commit at 6/27/2025, 4:17:10 PM
2025-06-27 16:21:15 -07:00
Expo CI 683a778b15 update CHANGELOG.md 2025-06-25 20:35:04 +00:00
Stanisław Chmiela 43ccaa6129 v16.13.1 2025-06-25 22:29:32 +02:00
Stanisław Chmiela d2c4608f3f [eas-cli] Add groups output to submit:internal (#3075)
<!-- 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

Since https://linear.app/expo/issue/ENG-14634/add-support-for-groups-option-in-fastlane-pilot-call we support passing `--groups` parameter to `eas submit` and configuring submission profile with `groups` parameter ("undocumentedly").

I need to add support for this property to `submit:internal`. We're going to source the `groups` to pass to `fastlane pilot` from either:
- submit profile (this pull request adds this)
- or workflow config (in which case we'll don't use this output).

# How

Added `groups` output to the `submit:internal` JSON object. We're going to parse it in https://github.com/expo/universe/blob/5dcea35fb50bf9fc8e3ae4ffe445f958bb5789e6/server/www/src/data/entities/workflow/job/WorkflowSubmitJob.ts#L192-L200.

# Test Plan

Ran `easd submit:internal --id 60a2d86a-b6d8-4ba2-b7dd-bf05bdd78791 --platform ios` in a test project of mine after adding submit profile like

```
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "6769090000",
        "groups": ["family", "friends"]
      }
    }
  }
```

<img width="253" alt="Zrzut ekranu 2025-06-25 o 17 39 22" src="https://github.com/user-attachments/assets/584e1bea-a905-4ba3-a68e-5df4a4d3477e" />
2025-06-25 22:10:10 +02:00
Will Schurman 🦦 d6b2caa4d6 [eas-cli] Fix update group deletion (#3069)
<!-- 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

Update group background job polling (and all background job polling in the CLI) is broken due to query caching in the client. Not sure why this worked when I added it as the caching has been present for a while.

# How

Disable cache for the poll request.

This also inverts the confirmation on `update:delete` to be affirmative as most of our other confirm questions are (default to yes). The reason for this change is that I found it non-intuitive while testing this to have "yes" cancel the command. I think the reason we use to have this inverted is it was dangerous, but IMO it's already enough intent by running the command to just have the default be yes. Happy to revert this back if reviewers disagree.

# Test Plan

```
eas update
neas update:delete <update_group>
```

See it now works.
2025-06-24 11:33:43 -07:00
Will Schurman 🦦 78cd4bf86a [eas-cli] Fix bug in log during update:republish (#3067)
<!-- 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

Noticed this condition was inverted during creation of a new related command.

# How

Correct the if statement. It should warn when platform is not `all`.

# Test Plan

Run `neas update:republish --platform ios`, see logged message:

```
$ eas update:republish --platform ios
✔ Find update by branch or channel? › Branch
✔ Select branch from which to choose update › main
✔ Load more update groups? › [Jun 23 11:14 by wschurman, runtimeVersion: 1.0.0] Initial commit

Generated by create-expo-app 3.4.2.
You are republishing an update group that wasn't published for all platforms.
✔ The republished update group will appear on the same platforms it was originally published on: ios
✔ Provide an update message. … Republish "Initial commit

Generated by create-expo-app 3.4.2." - group: 8e1b11be-ef82-4ccd-8294-a4e7075849d1
✔ Republished update group
```
2025-06-24 11:29:53 -07:00
Expo CI ce8e098a86 update CHANGELOG.md 2025-06-24 15:12:42 +00:00
Vojtech Novak 94494489de v16.13.0 2025-06-24 16:52:08 +02:00
Vojtech Novak 50ad49cf96 [eas-cli] add --what-to-test option to build (#3065)
* [eas-cli] add --what-to-test param to build

* improve command description
2025-06-24 13:51:10 +02:00
Douglas LowderandStanisław Chmiela 6a378b369b [eas-cli] hide workflow:list (#3060)
* [eas-cli] hide workflow:list

* Apply suggestions from code review

Co-authored-by: Stanisław Chmiela <[email protected]>

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
2025-06-20 12:18:32 -07:00
Douglas LowderandStanisław Chmiela 3f41cdd58e [eas-cli] align text of messages from build and workflow:run (#3061)
* [eas-cli] align text of messages from build and workflow:run

* Apply suggestions from code review

Co-authored-by: Stanisław Chmiela <[email protected]>

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
2025-06-20 12:15:38 -07:00
Expo CI ec38cdb154 update CHANGELOG.md 2025-06-20 12:39:17 +00:00
Stanisław Chmiela 2b89aed724 v16.12.0 2025-06-20 14:15:13 +02:00
Stanisław Chmiela b9334ab7a3 [eas-cli] Add projectRootDirectory to workflow:run payload (#3058)
<!-- 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

[exponent-internal.slack.com/archives/C06EFBQK3B7/p1750180529429209](https://exponent-internal.slack.com/archives/C06EFBQK3B7/p1750180529429209)

# How

Copied what we do for builds — https://github.com/expo/eas-cli/blob/b0522484afdbc7c80c7da84c724480ade8f16f55/packages/eas-cli/src/build/ios/prepareJob.ts#L37-L38 — and added `projectRootDirectory` to workflow runs.

I hoped there's a `node:path` method that would do what `slash` does, but I found none and Cursor did not know of one either.

# Test Plan

Tested end to end with https://github.com/expo/universe/pull/20644.
2025-06-20 14:14:23 +02:00
Vojtech Novak 0f717c0f22 v16.11.0 (#3059) 2025-06-18 18:09:51 +02:00
Vojtech Novak 537f95aa7b [eas-cli] add --what-to-test param to submit (#3023) 2025-06-18 16:14:09 +02:00
Expo CI 1dee08def7 update CHANGELOG.md 2025-06-13 12:45:51 +00:00
Stanisław Chmiela 15f6b04daa v16.10.1 2025-06-13 14:21:54 +02:00
Sebastian Szewczyk c0f27b8201 [eas-cli] eas build:dev command will correctly use passed build profile (#3053)
# Why

I tried to use eas dev:build with --build-profile parameter, but I got eas build with different profile

# How

I tested locally that changes in this branch fixes the problem for my case

# Test Plan

I have run following command (easd linked to locally build eas-cli, as described in CONTRIBUTING.md)
```
EXPO_DEBUG=1 easd build:dev -e preview -p android
```
2025-06-13 14:21:07 +02:00
Will Schurman 430181893f [eas-cli] Fix changelog merge conflict (#3052)
<!-- 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

6636f76cff had a logical merge conflict in the changelog since a new version was released.

# How

Move the entry to the correct release.

# Test Plan

Inspect.
2025-06-12 09:44:55 -07:00
Will Schurman 6636f76cff [eas-cli] Allow rolling out update on empty branch (#3050)
<!-- 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

This is the eas-cli component of https://github.com/expo/universe/pull/20493, which allows creating a rollout update on a branch with no existing updates.

# How

Remove error when no existing update exists.

# Test Plan

```
yarn create expo-app test-kjhsadjhkkjhafskjhjkhhhh
cd test-kjhsadjhkkjhafskjhjkhhhh
neas init
neas update:configure
neas update --rollout-percentage=60
```

View published update in db, see it has 60% rollout percentage but no control
2025-06-12 09:41:13 -07:00
Expo CI d7b47d080c update CHANGELOG.md 2025-06-12 09:21:13 +00:00
Douglas Lowder 046d8d75bb v16.10.0 2025-06-12 02:15:20 -07:00
Douglas Lowder 2d1f913648 [eas-cli] Implement workflow:cancel (#3048) 2025-06-11 13:03:02 -07:00
Kudo Chien b0522484af [eas-cli] drop repack support (#3044)
# Why

since the onboarding repack is not available now, let's remove the code for onboarding repack. we can later use `BuildMode.Repack` for workflow repack job.

# How

remove repack relevant code

# Test Plan

ci passed
2025-06-09 20:50:11 +08:00
Expo CI 28ee6b95c9 update CHANGELOG.md 2025-06-06 16:11:08 +00:00
Stanisław Chmiela 67d85c2025 v16.9.0 2025-06-06 17:44:31 +02:00
Piotr SzeremetaandStanisław Chmiela d213040dc4 Allow specifying groups option in submit command (#2891)
# Why

[ENG-14634: Add support for `groups` option in `fastlane pilot` call](https://linear.app/expo/issue/ENG-14634/add-support-for-groups-option-in-fastlane-pilot-call)

User should be able to pass the `groups` option to `fastlane pilot` when submitting an iOS build.

# How

* Accept the `groups` flag in the `eas submit` command
* Pass this parameter to the `submission-config`

# Test Plan

1. Build the project
2. Run the `eas submit -p ios --groups "Test 1"` command
3. Check the submission logs

# Deployment plan

1. https://github.com/expo/turtle-v2/pull/2082
2. https://github.com/expo/universe/pull/18600
3. https://github.com/expo/eas-cli/pull/2891

Co-authored-by: Stanisław Chmiela <[email protected]>
2025-06-06 17:33:37 +02:00
Douglas Lowder dda4fe45be [eas-cli] Fix workflow:runs query sort order (#3045) 2025-06-04 13:10:56 -07:00
Expo CI d5c0b1b3d7 update CHANGELOG.md 2025-06-04 11:33:53 +00:00
Stanisław Chmiela f968654ffc v16.8.0 2025-06-04 13:27:33 +02:00
Stanisław Chmiela a66753cde9 [eas-cli] Allow skipping version check (#3041)
<!-- 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/C089ED313PV/p1748545207578949?thread_ts=1748543276.564549&cid=C089ED313PV

# How

Added support for `EAS_SKIP_CLI_VERSION_CHECK` environment variable.

# Test Plan

Added tests.
2025-06-04 13:26:57 +02:00
Douglas LowderandWill Schurman 0c85c68948 [eas-cli] workflow:list and workflow:runs commands (#3030)
Co-authored-by: Will Schurman <[email protected]>
2025-06-03 19:41:31 -07:00
Will Schurman 78770912ab [eas-cli] Change location of EAS Update host override config (#3042)
<!-- 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

As requested during feature review, these should live in the top-level "cli" key of eas.json.

Closes ENG-15925.

# How

Change location.

# Test Plan

`yarn tsc`
2025-06-03 08:53:19 -07:00
Expo CI 727dc105f8 update CHANGELOG.md 2025-06-03 14:05:18 +00:00
Stanisław Chmiela e63de21702 v16.7.2 2025-06-03 15:58:23 +02:00
Stanisław Chmiela e3bbdd519e [eas-cli] Add support for array flavorDimensions (#3043)
# Why

https://exponent-internal.slack.com/archives/C06EFBQK3B7/p1748956922999689

# How

Added support for `flavorDimensions` specified as array of strings.

# Test Plan

Added tests.
2025-06-03 15:56:54 +02:00
Expo CI 25cc555189 update CHANGELOG.md 2025-06-03 07:25:43 +00:00
Stanisław Chmiela be73d3a337 v16.7.1 2025-06-03 09:19:10 +02:00
Stanisław Chmiela ecb6458a28 [eas-cli] Do not require eas.json or package.json to create a workflow run (#3028)
<!-- 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

`eas.json` is not _required_ to run workflows. Builds will fail, but we can warn about it and also the workflow error makes sense.

# How

Check before uploading the non-required files.

# Test Plan

<img width="946" alt="Zrzut ekranu 2025-06-2 o 21 45 31" src="https://github.com/user-attachments/assets/b6b4aad1-608f-471c-b541-302850744062" />
2025-06-03 09:18:17 +02:00
Will Schurman 475d90ea23 [eas-cli] Fix update republishing not including environment or git info (#3036)
<!-- 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

These fields were missing in republished updates.

Closes ENG-15846.

# How

Copy these fields over as well when republishing.

# Test Plan

`neas update` with dirty working tree.
`neas update:republish`

Verify republished update has same dirty working tree status.
2025-06-02 08:28:21 -07:00
Douglas Lowder 59171230d4 [eas-cli] Fix failing unit tests (#3035) 2025-05-29 14:10:58 -07:00
Expo CI b8b16139bf update CHANGELOG.md 2025-05-29 10:30:14 +00:00
Quinlan Jung 24b725438e v16.7.0 2025-05-29 12:24:48 +02:00
Quinlan Jung 3d2454ebf2 [eas-cli] add rollout flag to update:republish (#3029)
* [eas-cli] add rollout flag to update:republish

* Temporary Commit at 5/29/2025, 12:17:09 PM
2025-05-29 12:21:56 +02:00
Will Schurman 5caef3bc05 [eas-json][eas-cli] Add support for manifest host and asset host overriding (#3021)
<!-- 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

CLI change corresponding to https://github.com/expo/universe/pull/20131.

This adds the ability to override the manifest and asset hosts for EAS Update such that a set of proxy hosts can be used.

Closes ENG-15804.

# How

Considered putting these in flags, but it turned out that many many commands need to detect EAS Update configuration so they need access to these as well. Therefore, I added the new fields to `eas-json`.

The fields are:
- `manifestHostOverride` - a host to replace `u.expo.dev` in the URL.
- `assetHostOverride` - a host to replace `assets.eascdn.net` in asset URLs in the generated update manifests.

Then, in every place in EAS CLI when we generate a manifest URL, instead use the overridden URL.

Then, supply the manifest URL and asset URL overrides when creating updates (or republishes).

# Test Plan

Lengthy, but here it goes:
1. Create two proxies, one for assets and one for manifests. All these proxies do is proxy the requests.
    - https://github.com/wschurman/updates-test-proxy is a proxy for the manifest request built using expo router API routes and EAS Hosting. It is hosted at https://updates-test-proxy.expo.app and just fetches the URL input but at `u.expo.dev` and returns the response.
    - https://github.com/wschurman/updates-test-asset-proxy is the same but for assets. https://updates-test-asset-proxy.expo.app
2. Create an app on staging
3. Add the following to eas.json:
    ```
    "update": {
        "assetHostOverride": "updates-test-asset-proxy.expo.app",
        "manifestHostOverride": "updates-test-proxy.expo.app"
      }
    ```
4. run `neas update:configure`, see overridden URL in app.json
5. run `neas update`, see update is published, visit overridden manifest URL, see an update!
6. Visit one of the asset URLs (supplying authorization header) and see it correctly proxies.
7. configure code signing
8. publish again, see code signing works (downloads from `manifestPermalink`)
9. republish the last update, see it works and sets the overrides correctly.
2025-05-23 10:50:58 -07:00
Expo CI cc987ab71c update CHANGELOG.md 2025-05-23 17:45:39 +00:00
Will Schurman 32d3020659 v16.6.2 2025-05-21 09:05:18 -07:00
Will Schurman 17db11fa72 [eas-cli] Further no longer require owner field for SDK >= 53 or canary (#3017) 2025-05-16 17:19:51 -07:00
Expo CI b5305a38cc update CHANGELOG.md 2025-05-14 19:54:34 +00:00
Quinlan Jung 3ec7bb7334 v16.6.1 2025-05-14 12:47:29 -07:00
Quinlan Jung ceb07d4f54 [eas-cli] onboarding: add cwd to spawned git process (#3015)
* [eas-cli] onboarding: add cwd to spawned git process

* Temporary Commit at 5/14/2025, 12:39:27 PM
2025-05-14 12:44:22 -07:00
Expo CI 92e12a57a7 update CHANGELOG.md 2025-05-14 03:56:06 +00:00
Gabriel Donadel 373a4d40fa v16.6.0 2025-05-14 00:50:57 -03:00
Gabriel Donadel Dall'Agnol 568faf7697 [eas-cli] Add support for uploading for ipa files (#3014)
* [eas-cli] Add support uploading for ipa files

* Add changelog entry

* Add appIdentifier and appName to metadata
2025-05-14 05:49:05 +02:00
Expo CI df6ff1bb7a update CHANGELOG.md 2025-05-13 09:10:26 +00:00
Stanisław Chmiela a4a7b1c57f v16.5.0 2025-05-13 11:01:39 +02:00
Stanisław Chmiela a9f8bfd1ff [eas-cli] Add --json flag to workflow:run (#3013)
# Why

Affirm asked for this.

# How

Added `--json` flag which is going to print out all useful job information.

# Test Plan

`--wait --json`:
```
{
  "id": "0196c564-5553-70ed-8835-adff6484e8cf",
  "name": "easd.yml",
  "status": "SUCCESS",
  "createdAt": "2025-05-12T16:46:49.171Z",
  "workflow": {
    "id": "48ee38cd-44d3-4359-8b35-d71297545ea5",
    "fileName": "easd.yml"
  },
  "jobs": [
    {
      "id": "0196c564-58c9-75da-a3ef-931c64b756a2",
      "key": "test",
      "name": "test",
      "type": "CUSTOM",
      "status": "SUCCESS",
      "outputs": {},
      "createdAt": "2025-05-12T16:46:50.057Z"
    }
  ],
  "url": "http://expo.test/accounts/stanley/projects/test-local/workflows/0196c564-5553-70ed-8835-adff6484e8cf"
}
```


`--json`:
```
{
  "id": "0196c563-5a25-75e1-9447-e35d82f8fef0",
  "url": "http://expo.test/accounts/stanley/projects/test-local/workflows/0196c563-5a25-75e1-9447-e35d82f8fef0"
}
```
2025-05-13 11:00:40 +02:00
Stanisław Chmiela e7f687b45d [eas-cli] Add --wait flag to workflow:run (#3012)
<!-- 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/C083Q88U4M8/p1747060067200209

# How

Added `--wait`/`--no-wait` flag to `workflow:run`. It's off by default (keeping current behavior).

# Test Plan

```
Compressing project files and uploading to EAS. Learn more: https://expo.fyi/eas-build-archive
✔ Uploaded project archive to EAS 1s
✔ Uploaded eas.json to EAS 
✔ Uploaded package.json to EAS 

See logs: http://expo.test/accounts/stanley/projects/test-local/workflows/0196c530-e558-7357-85ab-139314b21171
✔ Workflow run started successfully.
```

```
See logs: http://expo.test/accounts/stanley/projects/test-local/workflows/0196c52f-0536-774a-8639-1e9de629d88e

Waiting for workflow run to complete. You can press Ctrl+C to exit.
✖ Workflow run has failed.
```

```
See logs: http://expo.test/accounts/stanley/projects/test-local/workflows/0196c52d-2065-7bb3-85d0-571d8d166d82

Waiting for workflow run to complete. You can press Ctrl+C to exit.
⚠ Workflow run has been canceled.
```

```
See logs: http://expo.test/accounts/stanley/projects/test-local/workflows/0196c53e-8150-7c52-91d4-7610b38eee11

Waiting for workflow run to complete. You can press Ctrl+C to exit.
⠏ ⚠ Failed to fetch the workflow run status. Check your network connection.
```

```
See logs: http://expo.test/accounts/stanley/projects/test-local/workflows/0196c53e-8150-7c52-91d4-7610b38eee11

Waiting for workflow run to complete. You can press Ctrl+C to exit.
✖ Failed to fetch the workflow run status 6 times in a row. Aborting wait.
```

```
USAGE
  $ eas workflow:run [FILE] [--non-interactive] [--wait]

ARGUMENTS
  FILE  Path to the workflow file to run

FLAGS
  --non-interactive  Run the command in non-interactive mode.
  --[no-]wait        Wait for workflow run to complete

DESCRIPTION
  Run an EAS workflow

FLAG DESCRIPTIONS
  --[no-]wait  Wait for workflow run to complete

    Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
2025-05-12 23:41:34 +02:00
Gabriel Donadel 666a885ba5 update CHANGELOG.md 2025-05-10 19:52:22 -03:00
Gabriel Donadel 25c77d05f4 v16.4.2 2025-05-09 18:12:21 -03:00
Gabriel Donadel Dall'Agnol 4b26cea3bc [build-cache-provider] Use new buildCacheProvider key (#3011)
* [build-cache-provider] Use new buildCacheProvider key

* Add changelog entry
2025-05-09 21:40:42 +02:00
Stanisław Chmiela 4aaf486e5e Update CHANGELOG.md 2025-05-07 15:48:13 +02:00
Expo CI 9a254ccf2d update CHANGELOG.md 2025-05-07 13:41:19 +00:00
Stanisław Chmiela c8aef9dc3a v16.4.1 2025-05-07 15:35:39 +02:00
Stanisław Chmiela f5669de55f Fix changelog 2025-05-07 15:35:13 +02:00
Piotr Szeremeta f08965d9d2 [eas-cli] Allow any suffix for gradle command build type (#3003)
# Why

[ENG-15618: Support different suffixes of custom gradle commands](https://linear.app/expo/issue/ENG-15618/support-different-suffixes-of-custom-gradle-commands)

Currently when user specifies a custom gradle command that does not end with `*Release` or `*Debug` the command is not parsed correctly. Build type and flavour are not parsed, which causes problems with setting code version and app version down the line.

# How

- Changed the regexp that parses gradle command to be more flexible, allowing any suffix that starts with a capital letter.

# Test Plan

- add a custom gradle command to `eas.json`, for example 
```
 "development": {
      "android": {
        "gradleCommand": ":app:assembleAbcStaging"
      }
    },
```
- add a `buildType` and packageFlavor to `build.gradle` file:
```
productFlavors {  
        abc { // New flavor
            versionCode 3
            versionName "abc"
        }
    }
buildTypes {
        staging {}
        debug {... }
        release {...}
}
```

- start an android build from cli.
- started build should not warn about invalid configuration
- on website, build version should be set to the value assigned in flavour.
2025-05-07 15:33:53 +02:00
Expo CI 817ed771f5 update CHANGELOG.md 2025-05-02 12:49:52 +00:00
Gabriel Donadel da4c41b9b7 v16.4.0 2025-05-02 09:43:54 -03:00
Gabriel Donadel 6f47f5f691 Fix changelog 2025-05-02 09:43:27 -03:00
Gabriel Donadel Dall'Agnol 204cd32979 [build-cache-provider] Read types from @expo/config (#3005)
* [build-cache-provider] Read types from @expo/config

* Add changelog entry

* Fix lint
2025-05-02 13:33:28 +02:00
Gabriel Donadel f83c7d8455 Revert "Add changelog entry"
This reverts commit 5aedf13af1.
2025-04-30 16:32:51 -03:00
Gabriel Donadel 5aedf13af1 Add changelog entry 2025-04-30 16:16:46 -03:00
Gabriel Donadel 39030e9a3e Update readme 2025-04-30 16:15:30 -03:00
Gabriel Donadel Dall'Agnol 3ca333747f Add eas-build-cache-provider package (#3002)
* Add eas-build-cache-provider package

* Add comments about types

* Add dependencies

* Add changelog entry
2025-04-27 23:05:13 +02:00
Piotr Szeremeta 6997df3450 [eas-cli] Add --verbose-logs option to eas build command (#3000) 2025-04-25 12:00:32 +02:00
Expo CI f939f3ceda update CHANGELOG.md 2025-04-24 22:19:42 +00:00
Quinlan Jung 716728a624 v16.3.3 2025-04-24 15:11:13 -07:00
Kadi Kraman f8a01bade3 [eas-json] Add android sdk-52 image to schema (#2989)
* Add android sdk-52 image to schema

* Add changelog
2025-04-24 13:40:27 +01:00
Kadi Kraman 2cb426522d Update codementions (#3001)
* Update codementions

* Add codementions for hosting
2025-04-24 12:25:07 +01:00
Phil Pluckthun 573373066d [eas-cli] Add specific error for WAF block events (EAS Hosting) (#2998)
* Add specific error for WAF block events

* Add CHANGELOG entry

* Remove duplicate line (oops)

* Apply lints
2025-04-24 09:42:28 +01:00
Kadi Kraman feba42f92b [eas-cli] Detect when expo metro config is used (#2996)
* Detect when expo metro config is used

* Add changelog

* Ensure backwards compatibility for SDK 50 and below
2025-04-23 11:13:32 +01:00
Quinlan Jung f373c3fc6c [eas-cli] upload assetmap.json when publishing update (#2994)
* [eas-cli] upload assetmap.json when publishing update

* Temporary Commit at 4/21/2025, 11:53:48 AM
2025-04-21 11:59:58 -07:00
Expo CI e1f86d779e update CHANGELOG.md 2025-04-17 23:40:49 +00:00
Quinlan Jung c42a34092f v16.3.2 2025-04-17 16:35:40 -07:00
Quinlan Jung ced7f924e7 [eas-cli] change launch asset file extension (#2991)
* [eas-cli] change launch asset file extension

* Temporary Commit at 4/16/2025, 2:58:03 PM

* Temporary Commit at 4/16/2025, 3:23:17 PM
2025-04-16 15:34:34 -07:00
Gabriel Donadel Dall'Agnol 93e16284ae [eas-cli] Update upload command to display build info (#2990)
* [eas-cli] Update upload command to display build info

* Add changelog entry
2025-04-16 14:36:10 +02:00
Expo CI 379036a03d update CHANGELOG.md 2025-04-11 15:26:53 +00:00
Gabriel Donadel 6035ce06ef v16.3.1 2025-04-11 12:20:40 -03:00
Gabriel Donadel Dall'Agnol 0333fd3519 [eas-cli] Allow no in dev-client flag for build:download (#2985)
* [eas-cli] Allow no in dev-client flag for build:download

* Add changelog entry
2025-04-11 15:40:47 +02:00
Expo CI 20b48326a4 update CHANGELOG.md 2025-04-09 23:17:20 +00:00
Gabriel Donadel 199bb78b15 v16.3.0 2025-04-09 20:12:04 -03:00
Gabriel Donadel 7e71b8eacd [eas-cli] Fix upload command lint 2025-04-09 20:03:14 -03:00
Gabriel Donadel Dall'AgnolandQuinlan Jung 14bcf917a6 [eas-cli] Add non-interactive and json flags to upload command (#2983)
* [eas-cli] Add non-interactive and json flags to upload command

* Add changelog entry

* Add update command to readme

* Update packages/eas-cli/src/commands/upload.ts

Co-authored-by: Quinlan Jung <[email protected]>

---------

Co-authored-by: Quinlan Jung <[email protected]>
2025-04-10 00:55:14 +02:00
Gabriel Donadel Dall'Agnol 6ae19df305 [eas-cli] Add build:download command (#2982)
* [eas-cli] Add build:download command

* Fix json flag

* Address PR comments

* Add dev-client flag
2025-04-08 22:25:03 +02:00
Expo CI eedc841ea6 update CHANGELOG.md 2025-04-08 13:09:33 +00:00
Gabriel Donadel 98308bb3bd v16.2.2 2025-04-08 10:03:33 -03:00
Gabriel Donadel Dall'Agnol 8b9de84855 [eas-cli][upload] Filter out duplicates and compress apps (#2981)
* [eas-cli][upload] Filter out duplicates and compress apps

* Fix leftover code
2025-04-08 15:00:30 +02:00
Expo CI 496d6f5a6e update CHANGELOG.md 2025-04-04 11:33:50 +00:00
Gabriel Donadel 89d1572f9d v16.2.1 2025-04-04 08:28:37 -03:00
Gabriel Donadel Dall'AgnolandSzymon Dziedzic 76f591583d Add upload command (#2932)
* Add share command

* Use EasShareGcsAppArchive upload type

* Add share build mutation

* Fix app file extension not persisting

* Add function to extract App Metadata

* Add tar support

* Use shareLocalBuild

* Add fingerprint flag

* Rename shareLocalBuildAsync to createLocalBuildAsync

* Fix fingerprint value

* Update packages/eas-cli/src/commands/share.ts

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

* Rename command to upload

* Remove ipa support

* Fix detecting ios build

---------

Co-authored-by: Szymon Dziedzic <[email protected]>
2025-04-04 13:07:07 +02:00
Evan Bacon 5d40cc1bc0 chore: bump expo/apple-utils (#2973)
* bump apple utils

* update async types

* Update telemetry.ts

* Update CHANGELOG.md
2025-04-03 17:31:43 -05:00
Expo CI 60a349f2f5 update CHANGELOG.md 2025-04-03 16:47:03 +00:00
Will Schurman 439d45d6e1 v16.2.0 2025-04-03 09:37:07 -07:00
Quinlan Jung cc48482595 [eas-cli] update: add warning if no build exists with fingerprint (#2977)
# Why

When publishing updates, users need to know if there are compatible builds for their fingerprints. This PR prints this information at the end of `eas update` to warn users, if they have updates being published that don't have corresponding builds, causing potential compatibility issues.

<img width="1536" alt="Screenshot 2025-04-01 at 5 09 12 PM" src="https://github.com/user-attachments/assets/400ac0d9-c39e-4aee-9793-465c6acb634e" />


# How

Added functionality to check for compatible builds when publishing updates:

1. Enhanced `fetchBuildsAsync` to support filtering by `fingerprintHash`
2. Implemented `findCompatibleBuildsAsync` to find builds matching fingerprints
3. Added warnings in the update publish command when no compatible builds are found for fingerprints
4. Included links to the fingerprint details on the Expo website for easier debugging

# Test Plan

Run `eas update` with a fingerprint that doesn't have a compatible build
2025-04-02 15:41:18 -07:00
Quinlan Jung a4fb2e20d3 [eas-cli] remove giant else stmt in update routin (#2976)
# Why

This PR refactors the `UpdatePublish` command to improve code readability and flow by removing a nested `else` block and using an early return pattern.

before
```
 if (jsonFlag) {
      printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
 } else {
      // all the logic
 }
```

after
```
 if (jsonFlag) {
      printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
      return;
 } 

// all the logic
```

# How

- Removed the large `else` block that contained most of the code in the `UpdatePublish` command
- Added an early return after handling the JSON flag case
- Kept all the existing functionality intact while making the code more maintainable
- Improved the logical flow by flattening the code structure

# Test Plan

Manually tested the `eas update` command to ensure the code path works correctly
2025-04-02 15:40:06 -07:00
Will Schurman ec16d92724 [eas-cli] Switch update group deletion to be asynchronous (#2975) 2025-04-01 14:07:18 -07:00
Stanisław Chmiela 34e7720e6f [eas-cli] Add helpful info for ScopedAccountActorViewerContext errors in EAS (#2971)
<!-- 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'm going to move Workflow and GitHub builds to scoped token soon. If users depend on the automatically provided `EXPO_TOKEN` _and_ the action they're going to try to do has not been whitelisted with scoped rules yet, they're going to get an `UnauthorizedError` mentioning `ScopedAccountActorViewerContext`.

On top of logs annotations (working on them [here](https://github.com/expo/universe/pull/19494)) I think we can make the error message a bit better.

# How

Special-case `ScopedAccountActorViewerContext` error in `process.env.EAS_BUILD` and print message asking the user to contact us.

Alongside this, I think https://github.com/expo/universe/pull/19500 may be a good idea.

# Test Plan

With `readRules` in `UserPrivacyPolicy` cleared out:

<img width="1440" alt="Zrzut ekranu 2025-03-31 o 23 43 00" src="https://github.com/user-attachments/assets/89245ccc-21e7-4041-98ce-21a517f932d4" />
2025-04-01 17:32:08 +02:00
Szymon Dziedzic 938511d5fd [gha] remove Szymon from codemention (#2970) 2025-03-31 16:09:37 +02:00
Stanisław Chmiela b473e72c73 [eas-cli] Improve unauthorized error for eas deploy --prod (#2968)
<!-- 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

When we move Workflows to scoped token which has at most DEVELOPER permission, `type: deploy, prod: true` jobs will fail with an ominous

```
You don't have the required permissions to perform this operation.

This can sometimes happen if you are logged in as incorrect user.
Run eas whoami to check the username you are logged in as.
Run eas login to change the account.

Original error message: Entity not authorized: AppDevDomainNameEntity[00000000-0000-0000-0000-000000000000] (viewer = ScopedAccountActorViewerContext[16d62ef3-a219-47ca-936b-89958f6cb1f8], action = CREATE, ruleIndex = -1)
Request ID: 863a1317-882b-4cab-b8f2-c5f5d63b6f29

```

# How

Special-cased the `--prod` && `AppDevDomainEntity` && `CREATE` && `UNAUTHORIZED_ERROR` error.

Not sure if we should call out "as an app admin" in the error message. Open to suggestions.

# Test Plan

Ran `eas deploy --prod` as a developer-permission-only user. I've updated message in code since taking the screenshot.

<img width="836" alt="Zrzut ekranu 2025-03-28 o 21 21 02" src="https://github.com/user-attachments/assets/fc3a8b08-ae2f-4630-9e88-ccb48151d3e2" />
2025-03-31 13:52:46 +02:00
Quinlan Jung 2816b432d8 [eas-cli] add build-profile flag (#2966)
* [eas-cli] add build-profile flag

* Temporary Commit at 3/26/2025, 4:21:14 PM

* Temporary Commit at 3/27/2025, 4:23:26 PM
2025-03-27 16:30:47 -07:00
Quinlan Jung 56f0f7792d [eas-cli] remove hidden flag from fingerprint:generate (#2965)
* [eas-cli] remove hidden flag from  fingerprint:generate

* Temporary Commit at 3/26/2025, 1:16:34 PM
2025-03-27 09:47:10 -07:00
Quinlan Jung 5236de797b Temporary Commit at 3/21/2025, 12:56:52 PM (#2954) 2025-03-26 12:34:32 -07:00
Expo CI fb33298024 update CHANGELOG.md 2025-03-26 12:51:26 +00:00
Szymon Dziedzic 66c6c8ab0b v16.1.0 2025-03-26 13:46:22 +01:00
Szymon Dziedzic 552ef1afe9 [eas-cli] add corepack field to eas.json (#2964)
<!-- 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

Closes https://github.com/expo/eas-cli/issues/2401

# How

Add `corepack` field to `eas.json`

# Test Plan

Tests, tested manually
2025-03-26 13:45:29 +01:00
Expo Bot b35a4efdaa Upgrade eas-build dependencies (#2961) 2025-03-25 11:42:17 +01:00
Expo Bot 7244fc83dc Upgrade eas-build dependencies (#2956) 2025-03-24 11:16:08 +01:00
Quinlan Jung 1da9573040 [eas-cli] add support for build profiles in fingerprint generation (#2951)
* [eas-cli] add build profile support for fingerprint

Temporary Commit at 3/16/2025, 6:31:01 PM

Temporary Commit at 3/16/2025, 6:37:24 PM

Temporary Commit at 3/16/2025, 7:59:29 PM

Temporary Commit at 3/16/2025, 8:01:21 PM

Temporary Commit at 3/18/2025, 2:38:20 PM

Temporary Commit at 3/18/2025, 3:21:52 PM

* Temporary Commit at 3/20/2025, 3:35:30 PM

* Temporary Commit at 3/20/2025, 3:55:17 PM

* Temporary Commit at 3/21/2025, 7:42:01 AM

* Temporary Commit at 3/21/2025, 7:42:34 AM

* Temporary Commit at 3/21/2025, 7:46:59 AM

* Temporary Commit at 3/21/2025, 12:16:23 PM
2025-03-21 12:21:14 -07:00
Expo CI 4e8fc059a4 update CHANGELOG.md 2025-03-20 21:48:51 +00:00
Brent Vatne 6bb3dd94f7 v16.0.1 2025-03-20 14:43:51 -07:00
Brent Vatne 079328cf5a [eas-cli] Always sync update configuration to native with eas update:configure (#2957) 2025-03-20 14:43:12 -07:00
dependabot[bot] 4ff8415f71 Bump @babel/runtime from 7.12.5 to 7.26.10 (#2949)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.12.5 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 11:11:19 +01:00
Expo CI e8d14557d6 update CHANGELOG.md 2025-03-19 09:36:29 +00:00
Stanisław Chmiela 924f51ecb6 v16.0.0 2025-03-19 10:31:01 +01:00
Quinlan Jung f297df7f67 [ENG-15002][eas-cli] fingerprint:generate (#2937)
* [eas-cli] fingerprint:generate

* Temporary Commit at 3/16/2025, 6:31:01 PM

* Temporary Commit at 3/16/2025, 6:37:24 PM

* reorganize fingerprint utils

* Temporary Commit at 3/17/2025, 4:24:33 PM

* Temporary Commit at 3/17/2025, 4:34:41 PM

* Temporary Commit at 3/17/2025, 4:51:09 PM
2025-03-17 17:11:30 -07:00
Stanisław Chmiela ef8d6d08bc [eas-cli] Adhere to .easignore even if requireCommit is true (#2942)
# Why

https://github.com/expo/eas-cli/pull/2925#issuecomment-2710138932

# How

Delete files from `.easignore` even if `requireCommit` is `true`. Instead of erroring, print a warning.

# Test Plan

Adjusted tests.
2025-03-17 18:38:24 +01:00
Mike Hampton 6ba59a321b [ci] Pin to older tj-actions/changed-files commit. (#2948) 2025-03-15 08:01:08 -04:00
Stanisław Chmiela 6e87e94ab3 Do not warn about EAS_PROJECT_ROOT multiple times (#2943)
# Why

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

# How

Made it so we don't print warning about `EAS_PROJECT_ROOT` multiple times.

# Test Plan

CI should pass.
2025-03-12 17:26:27 +01:00
Will Schurman 0dbb6890b2 [eas-cli] Add requestId to ApiV2Error (#2941) 2025-03-12 08:44:43 -07:00
dependabot[bot] 2b228272c8 Bump @babel/helpers from 7.16.5 to 7.26.10 (#2940)
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.16.5 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 14:43:34 +01:00
Szymon Dziedzic b5077bc4fd [eas-cli] use correct logic to determine whether artifacts expired in eas build:run (#2931)
* [eas-cli] use correct logic to determine whether artifacts expired in eas build:run

* add changelog

* Update packages/eas-cli/src/run/utils.ts
2025-03-12 11:22:24 +01:00
Expo CI b07315e32d update CHANGELOG.md 2025-03-12 00:07:18 +00:00
Brent Vatne 3c5bb3e815 v15.0.15 2025-03-11 16:57:50 -07:00
Brent Vatne 5755dfdd3a [eas-cli] Pass through the updates version to setUpdatesConfigAsync (#2934) 2025-03-06 16:47:32 -08:00
Expo CI d9e66437f4 update CHANGELOG.md 2025-03-06 14:04:41 +00:00
Szymon Dziedzic faa75393a2 v15.0.14 2025-03-06 14:59:08 +01:00
Szymon Dziedzic 759d749a76 [eas-cli] suggest using eas build:dev when using simulator/emulator dev client configuration (#2929)
* [eas-cli] suggest using eas build:dev when using simulator/emulator dev client configuration

* add changelog
2025-03-06 14:58:12 +01:00
Stanisław ChmielaandSzymon Dziedzic 34c307c829 [eas-cli] Use Git to resolve root path in no-VCS mode (#2904)
<!-- 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 was thinking we can offer `EAS_NO_VCS=1` as a simple escape hatch for people that are getting weird Git-related issues. One of them may be an old version of Git that does not support our `git clone` properly.

# How

In `getRootPath` of no-VCS mode, return first of those values (in order):
- `EAS_PROJECT_ROOT`
- Git root path
- `process.cwd()`

# Test Plan

Added a basic test.

Co-authored-by: Szymon Dziedzic <[email protected]>
2025-03-04 13:15:48 +01:00
Expo CI 7b101601f2 update CHANGELOG.md 2025-03-04 08:15:59 +00:00
Szymon Dziedzic 668aeb2569 v15.0.13 2025-03-04 09:10:05 +01:00
Szymon Dziedzic 1f68e225d3 upgrade eas-cli-local-build-plugin to 1.0.171 2025-03-04 09:09:57 +01:00
Expo Bot c530d06e22 Upgrade eas-build dependencies (#2927) 2025-03-03 16:24:16 +01:00
Stanisław Chmiela 9bfc44bdbc [eas-cli] Fix .git x .easignore interaction (#2925)
<!-- 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

Thanks to [a conversation on Bluesky](https://bsky.app/profile/sjchmiela.bsky.social/post/3lj6k2xo7rk2g) it turned out [this check](https://github.com/expo/eas-cli/blob/3a434940eb44dfa8dddc42af055793134ca9016f/packages/eas-cli/src/vcs/clients/git.ts#L255-L257) is invalid (always true), because `Ignore` has `DEFAULT_IGNORE`, so even if a user had no `.git` in `.easignore`, especially if they had `requireCommit: true`, `.git` would be removed.

# How

- Added a check to disallow `.easignore` if one has `requireCommit: true`.
- Changed logic around `Ignore` so we have separate `createForCopying` (ignores `.git` and `node_modules` always) and `createForChecking` (does not ignore anything by default). I don't like it, but it should work and is the best of what I could come up with without further refactoring.

# Test Plan

Added more tests.
2025-02-28 15:32:27 +01:00
Kadi Kraman d11e43f8e0 [eas-cli] improve applicationIdSuffix error message (#2924)
* Add productFlavors suggestion to error message

* Add changelog

* Lint
2025-02-28 12:14:17 +00:00
Quinlan Jung c19a737287 [eas-cli] amend fingerprint compare url (#2923)
* [eas-cli] amend fingerprint compare url

* Temporary Commit at 2/27/2025, 11:39:02 AM
2025-02-27 11:51:56 -08:00
Szymon Dziedzic 2790fd4cd5 use lowercase commands descriptions (#2907) 2025-02-27 10:18:44 +01:00
Quinlan Jung 1b713af42f [eas-cli] fix git diff header in fingerprint:compare (#2915)
* [eas-cli] fix git diff header in fingerprint:compare

* Temporary Commit at 2/24/2025, 12:01:45 PM
2025-02-24 12:48:11 -08:00
Quinlan Jung 3ac6472682 [eas-cli] fix formatFields to handle empty array (#2914)
* [eas-cli] fix formatFields to handle empty array

* Temporary Commit at 2/24/2025, 11:40:14 AM
2025-02-24 11:53:02 -08:00
Quinlan Jung 4b8a9d195f [eas-cli] lowercase urls, add more pretty prints (#2909)
* [eas-cli] lowercase urls, add more pretty prints

* Temporary Commit at 2/14/2025, 12:38:47 PM

* Temporary Commit at 2/14/2025, 12:41:29 PM

* Temporary Commit at 2/21/2025, 12:17:24 PM
2025-02-24 11:23:07 -08:00
Expo CI 30c5e9f4c1 update CHANGELOG.md 2025-02-22 01:46:31 +00:00
Brent Vatne d0c8f6bab7 v15.0.12 2025-02-21 17:40:05 -08:00
Evan Bacon faf6bffc62 [cli] bump app store to fix key api (#2916) 2025-02-21 17:39:05 -08:00
Expo CI ad55fdae37 update CHANGELOG.md 2025-02-21 13:59:33 +00:00
Szymon Dziedzic b5f77404cb v15.0.11 2025-02-21 14:53:36 +01:00
Szymon Dziedzic a1d4f26236 [eas-cli] skip creation of testflight group when there are already exisitng testflight groups + allow to opt out of the behavior by setting env var (#2856)
* [eas-cli] skip creation of testflight group when there are already exisitng testflight groups + allow to opt out of the behavior by setting env var

* rename env var

* add changelog
2025-02-21 14:49:32 +01:00
Quinlan Jung 6c7bae2349 [eas-cli] add oxford comma (#2908)
* [eas-cli] add oxford comma

* Temporary Commit at 2/14/2025, 10:00:01 AM
2025-02-14 10:20:50 -08:00
Evan Bacon d7e48c2894 fix(submit): Fix TestFlight internal group creation (#2906)
* Update ensureTestFlightGroup.ts

* Update CHANGELOG.md

* bump apple utils
2025-02-13 10:12:26 -08:00
Theo 918a7f9501 [eas-cli] Clarify error message regarding ignored files (#2905)
<!-- 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 struggled with adding a new workflow within `.eas/build` since I had `build/` in my `.gitignore`. I removed the line but it was still an issue. Turns out I had the same thing in `.easignore`. I think adding this to the error message could be helpful for others

# How

Not tested
2025-02-12 10:54:21 +01:00
Will Schurman 852dac030f [eas-cli] Narrow amount of data queried for basic update channel operations (#2901) 2025-02-11 20:07:36 -08:00
Expo CI 911cc90b93 update CHANGELOG.md 2025-02-11 19:13:45 +00:00
Stanisław Chmiela b9234abff1 v15.0.10 2025-02-11 20:07:50 +01:00
Stanisław Chmiela 574bde9831 [eas-cli] Do not include files deleted in working directory (#2900)
<!-- 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

When we `git clone` the source repository and then copy worktree changes over, we add and modify files. However, we do not _delete_ files that have been deleted in worktree.

# How

When we operate in `requireCommit: false` mode, we add `--no-checkout` to the `git clone` command. This way the destination directory only has `.git` directory when we start copying files over. Thus, files deleted from worktree will not suddenly appear in tarball.

In `requireCommit: true` mode we don't add `--no-checkout`, because we don't copy files.

# Test Plan

Added test. Tested manually.

Side effect of this is that `git status` reports that all files have been deleted and there's a bunch of untracked files (which, `git add`-ed, make the repository appear as the source repo). I don't know how to copy the Git index 1-1…
2025-02-11 15:46:48 +01:00
Expo CI 78837a413a update CHANGELOG.md 2025-02-09 12:18:18 +00:00
Stanisław Chmiela 6f63315f8e v15.0.9 2025-02-09 13:13:03 +01:00
Stanisław Chmiela e8ac97c8cc [eas-cli] Finally fix Windows not-ignoring files (#2894)
<!-- 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

With `toNamespacedPath` I was finally able to make ignoring files work properly.

# How

Removed `if (__NORMALIZE)` checks.

# Test Plan

Ran `eas build:inspect` on Windows, confirmed no `node_modules` was copied. Also ran `eas build:inspect` on macOS, worked too.
2025-02-09 13:10:14 +01:00
Expo CI a6fc31787d update CHANGELOG.md 2025-02-09 11:40:14 +00:00
Stanisław Chmiela 3056d8e23a v15.0.8 2025-02-09 12:19:05 +01:00
Stanisław Chmiela 1bf6757a72 [eas-cli] Try namespacing paths (#2893)
<!-- 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

`fs` adds a [Win32 File Namespace](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces) to the beginning of the path which breaks `path.relative` if `src` does not have the namespace.

<img width="899" alt="Zrzut ekranu 2025-02-9 o 12 15 45" src="https://github.com/user-attachments/assets/942b28d1-881f-4e5d-861e-297bb5eddbf2" />

# How

Ensured that running `toNamespacedPath` on an already namespaced path does not add a _second_ namespace:

```
> path.win32.toNamespacedPath("D:\\abc")
'\\\\?\\D:\\abc'
> path.win32.toNamespacedPath(path.win32.toNamespacedPath("D:\\abc"))
'\\\\?\\D:\\abc'
```

Wrapped `src` and `srcFilePath` (two arguments to `path.relative`) with `path.toNamespacedPath` so `path.relative` works.

# Test Plan

Will release and test.
2025-02-09 12:18:13 +01:00
Expo CI b948dd5674 update CHANGELOG.md 2025-02-09 10:05:08 +00:00
Stanisław Chmiela 0e7a8bfe5e v15.0.7 2025-02-09 10:59:55 +01:00
Stanisław Chmiela b86696fcdf [eas-cli] Allow normalizing root path to fix Windows not ignoring files (#2892)
<!-- 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

Since v15 files that should be ignored get added to the tarball on Windows.

# How

After adding some debugging I was able to find that `src`, `srcFilePath` don't match on path separators which breaks `ignore.ignores`.

<img width="827" alt="Zrzut ekranu 2025-02-7 o 21 57 02" src="https://github.com/user-attachments/assets/9ed3b166-dbc0-4287-87a1-0009769e0d4d" />

Added `path.normalize` if `__NORMALIZE` is `1`.

---

Also noticed we used wrong `cwd` for is-tracked check.

# Test Plan

Going to release this and test.
2025-02-09 10:58:50 +01:00
Expo CI f334bdbcc7 update CHANGELOG.md 2025-02-07 14:50:59 +00:00
Stanisław Chmiela f16433048a v15.0.6 2025-02-07 15:39:53 +01:00
Stanisław Chmiela e83061de36 [eas-cli] Add more logging around creating a tarball (#2890)
Should help debug why `node_modules` get included on Windows.
2025-02-07 15:39:21 +01:00
Stanisław Chmiela 73e0170836 Rephrase message at the end of workflow:run (#2886)
https://linear.app/expo/issue/ENG-14752/update-copy-at-the-end-of-workflowrun
2025-02-06 15:52:34 +01:00
Expo CI 078c7287d3 update CHANGELOG.md 2025-02-06 12:49:35 +00:00
Stanisław Chmiela a65e691325 v15.0.5 2025-02-06 11:16:44 +01:00
Stanisław Chmiela b306109676 [eas-cli] Skip makeShallowCopyAsync if requireCommit (#2885)
<!-- 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

`requireCommit: true` setups expect worktree to be clean (e.g. encrypted files to be locked).

# How

Made it so if `requireCommit` is true we don't copy files over.

# Test Plan

I have enabled `git-crypt` in my test repository, added a secret file, ran `easd build:inspect -p android -s archive -o ~/test-production --force`. The clone did contain secret file in plaintext.

Then I enabled `requireCommit: true` and repeated the command. The clone did contain the file encrypted.
2025-02-06 11:04:23 +01:00
Stanisław Chmiela 035ca03b96 [eas-cli] Fix deleting ignored submodules when archiving project (#2884)
<!-- 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

`git ls-files` returns submodules as a single filepath entry, so `rm` must remove it with `-rf`. Fixes https://github.com/expo/eas-cli/issues/2883.

# How

Added `{ recursive: true, force: true }` to `fs.rm`.

# Test Plan

I tried `easd build:inspect -p android -s archive -o ~/testtest --force` on a repo with a submodule in `.easignore`. It succeeded and the submodule was not included in the folder.
2025-02-06 10:45:46 +01:00
Expo CI b3d092423b update CHANGELOG.md 2025-02-05 10:49:05 +00:00
Stanisław Chmiela b768966ec4 v15.0.4 2025-02-05 11:34:13 +01:00
Stanisław Chmiela 1b40c1c20a [eas-cli] Raise threshold for "big tarball" warning (#2878)
<!-- 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

Adding `.git` directory to the tarball makes it bigger. Users notice this warning and want it to be actionable, but currently adding `.git` to `.easignore` does nothing.

# How

For now, raise threshold for warning from 100 MiB to 150 MiB. In the next pull request I'm going to fix `.git` exclusion via `.easignore`.

# Test Plan

CI should pass.
2025-02-05 11:33:12 +01:00
Stanisław Chmiela 98344b8fc2 [eas-cli] Support excluding .git via .easignore (#2879)
<!-- 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

Currently it's not possible to exclude `.git` from being uploaded in project tarball.

# How

In `makeShallowCopyAsync()` we first clone the repository, then we delete files ignored by `.easignore` and then we copy working directory over.

The "delete files ignored by `.easignore`" step is governed by `git ls-files --exclude-from=.easignore`. `git ls-files` is never going to consider `.git` directory to be excluded, even if specified in `.easignore`, so we need to special-case the `.git` directory and remove it manually if specified.

# Test Plan

Added `.git` to a test `.easignore`, ran `eas build:inspect` and confirmed the copied directory did _not_ contain `.git` folder.
2025-02-05 11:22:11 +01:00
Stanisław Chmiela f5480b4770 [eas-cli] Fix symlink support (#2874)
<!-- 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

`fs-extra` fails to copy symbolic links as one would expect. https://github.com/jprichardson/node-fs-extra/issues/708

# How

We could:
- copy and dereference symbolic links — make it so that the clone of the original directory has two copies of the files -- original and linked
- copy symbolic links without re-resolving, assuming any links will be relative and should work when unpacked on a different machine.

I think we can count on the latter.

# Test Plan

Created a link in my test repository

```
drwxr-xr-x    7 sjchmiela  staff   224B Jan 28 16:14 assets
lrwxr-xr-x    1 sjchmiela  staff     6B Feb  4 22:12 assets-linked -> assets
```

ran

```
easd build:inspect -p android -s archive -o ~/testtest --force
```

confirmed I see

```
drwxr-xr-x    7 sjchmiela  staff   224B Feb  4 22:20 assets
lrwxr-xr-x    1 sjchmiela  staff     6B Feb  4 22:20 assets-linked -> assets
```
in the result directory.
2025-02-05 11:00:32 +01:00
Stanisław Chmiela a2929ad364 [eas-cli] Improve DX of Ignore (#2877)
<!-- 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

`[email protected]` doesn't interpret `.easignore` right because I haven't initialized the `Ignore` class properly.

# How

Make it so it's not possible to use `Ignore` without initializing it.

# Test Plan

Tests should pass.
2025-02-05 10:54:35 +01:00
Stanisław Chmiela 6deee68a00 [eas-cli] Fix .easignore behavior for GitClient (#2873)
<!-- 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

If you had an `.easignore`, `isFileIgnoredAsync` was always thinking all files are NOT ignored which was causing it to think all builds are in `.GENERIC` mode (with native code) which was not true, because ACTUALLY the files WERE ignored.

All mostly because of forgotten `ignore.initIgnoreAsync()`…

# How

Added `ignore.initIgnoreAsync()`. Also realized whether the file is tracked / not-tracked may affect how actually is it going to behave, so added it to the mix.

# Test Plan

Added tests that I think test all cases.
2025-02-05 10:41:06 +01:00
Quinlan Jung 16d190ab84 [eas-cli] add --open flag to fingerprint:compare (#2872)
* [eas-cli] add --open flag to fingerprint:compare

* Temporary Commit at 2/4/2025, 12:47:25 PM
2025-02-04 19:40:18 -08:00
Will Schurman 6f2fc7c37b [eas-cli] No longer require owner field for SDK >= 53 or canary (#2835) 2025-02-04 18:40:17 -08:00
Quinlan Jung 1b78407f15 [eas-cli] make less gql calls fingerprint:compare (#2860)
* [eas-cli] make less gql calls fingerprint:compare

* Temporary Commit at 2/3/2025, 3:54:11 PM
2025-02-04 11:41:36 -08:00
Quinlan Jung 05c8fde824 [eas-cli] change url in fingerprint:compare (#2861)
* [eas-cli] change url in fingerprint:compare

* Temporary Commit at 2/3/2025, 4:15:22 PM

* Update CHANGELOG.md
2025-02-04 11:39:24 -08:00
Quinlan Jung d9af6cb301 [eas-cli] reapply website launcher (#2871)
* Reapply "[eas-cli] popup website in fingerprint:compare (#2859)" (#2862)

This reverts commit 528988dc0f.

* Temporary Commit at 2/4/2025, 11:36:07 AM
2025-02-04 11:37:29 -08:00
Expo CI 365e71f042 update CHANGELOG.md 2025-02-04 19:23:08 +00:00
Stanisław Chmiela e74c606efa v15.0.3 2025-02-04 20:16:41 +01:00
Kudo Chien 28f3a92c38 [eas-cli] pass server env when resolving fingerprint runtime version (#2867)
# Why

receiving an issue for a customer who uses eas environment and continuous-deploy-fingerprint github action. they have build variants setup in their app.config.ts. the app id, scheme and some other properties are determined by their `APP_VARIANT` variable. the `APP_VARIANT` is from eas server environment.

when continuous-deploy-fingerprint first calculates fingerprint, it uses `eas env:exec` for the `npx expo-updates fingerprint:generate` call. the `APP_VARIANT` is correctly passed.
then for `eas update` call, it doesn't pass the `APP_VARIANT` env and generates an inconsistent fingerprint.

# How

pass server env to the `getRuntimeVersionInfoObjectsAsync()` (`npx expo-updates runtimeversion:resolve`)
2025-02-05 00:30:15 +08:00
Expo CI be9fb59e6d update CHANGELOG.md 2025-02-04 12:18:36 +00:00
Szymon Dziedzic 365a7465f4 v15.0.2 2025-02-04 13:13:19 +01:00
Szymon Dziedzic 85a66629c8 [eas-cli] auto start dev client server at the end of eas build:dev (#2868)
<!-- 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

Auto-start dev client server at the end of `eas build:dev` command

# How

Auto-start dev client server at the end of `eas build:dev` command

# Test Plan

Tested manually

[Screen Recording 2025-02-04 at 13.00.38.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/9YRXgAETSTRMfjZ0IP35/fabf33a3-77e4-474f-a5fe-71725446769f.mov" />](https://app.graphite.dev/media/video/9YRXgAETSTRMfjZ0IP35/fabf33a3-77e4-474f-a5fe-71725446769f.mov)
2025-02-04 13:11:46 +01:00
Expo CI 8a2d68fcb5 update CHANGELOG.md 2025-02-04 11:40:56 +00:00
Szymon Dziedzic 62819e0956 v15.0.1 2025-02-04 12:35:39 +01:00
Szymon Dziedzic 5c6880f4ce [eas-cli] cache downloaded app archives in eas build:dev (#2864)
<!-- 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 don't need to redownload app every single time in `eas build:dev`. We can cache it locally. This functionality already exists for `eas build:run`.

# How

Use caching the same way we do for `eas build:run`

# Test Plan

Tested manually
2025-02-04 12:34:55 +01:00
Szymon Dziedzic d5f562edf8 [eas-cli] log time that it took to complete the previous build in eas build:dev (#2865)
<!-- 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

@brentvatne suggested that it would be useful to let people know how long the previous build took when rebuilding in `eas build:dev`

# How

Get the information about how long previous build took and show it to the user

# Test Plan

Tested manually
2025-02-04 12:34:29 +01:00
Expo CI 077d8b3d86 update CHANGELOG.md 2025-02-04 09:20:55 +00:00
Stanisław Chmiela 21c8131bcf v15.0.0 2025-02-04 10:14:54 +01:00
Stanisław Chmiela 18bf41782f [eas-cli] Use Git to pack Git projects (#2841)
<!-- 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

Currently, when `eas-cli` create a project tarball for EAS Build or Workflows, unless the user has `requireCommit: true` in `eas.json`, the project is being packed with a plain `makeShallowCopyAsync` which copies all the files that are deemed ignored by `ignore` package and `.easignore` or `.gitignore` files.

This is suboptimal for `eas workflow:run` experience — a developer with a project with Git expects the repository to exist on EAS.

# How

This was a big pain.

Previously, we had `GitClient` which required commit and its subclass, `GitNoCommitClient` which overrode some functions to allow uncommitted changes. On top of that, `GitClient` copied project with `git clone`, while `GitNoCommitClient` copied it with `makeShallowCopyAsync`. This meant `GitClient`'s copy would include `.git`, whereas `GitNoCommitClient` would not. Moreover, thus, `GitClient` would adhere to `.gitignore` and `GitNoCommitClient` would _not_ — only to `.easignore`.

This pull request:
- merges the two Git clients into one with `requireCommit` option
- this causes `.git` directory to exist in copies with `requireCommit = false`
- changes how we log changes caused by Git case sensitivity change (we need to handle the case where there is a preexisting dirty tree)
- after we clone the repository, we delete files that should have been ignored according to `.easignore`
- afterwards, we make a shallow copy of all the files over to the clone. This copies live changes to the working tree over to the clean clone. (They may only exist in `requireCommit = false` situation.)
- adds better support for `.easignore` to `isFileIgnoredAsync`.

As far as I can see the behavior looks like this:

![Bez tytułu-2025-01-11-0010](https://github.com/user-attachments/assets/3da4f36e-3f19-4573-b4e1-012faab7d4ef)

The breaking change in here is that we're going to start adhering to `.easignore` even if someone has `requireCommit = true`.

# Test Plan

I used `easd build:inspect` a bunch to verify it works like I expect. I encourage fellow reviewers to do the same.
2025-02-04 10:13:39 +01:00
Szymon Dziedzic 0513fc51a6 [eas-cli] print no vcs warning only once (#2863)
<!-- 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/p1738630158821649

# How

Print warn message only once

# Test Plan

Tests
2025-02-04 10:02:37 +01:00
Quinlan Jung 528988dc0f Revert "[eas-cli] popup website in fingerprint:compare (#2859)" (#2862)
This reverts commit 7f46baeb21.
2025-02-03 17:22:53 -08:00
Quinlan JungandWill Schurman 7f46baeb21 [eas-cli] popup website in fingerprint:compare (#2859)
* [eas-cli] popup website in fingerprint:compare

* Temporary Commit at 2/3/2025, 1:56:05 PM

* Update packages/eas-cli/src/commands/fingerprint/compare.ts

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

---------

Co-authored-by: Will Schurman <[email protected]>
2025-02-03 15:47:02 -08:00
Quinlan Jung 608d61ed69 [eas-cli] add interactive mode for fingerprint:compare (#2854)
* [eas-cli] add interactive mode for fingerprint:compare

* fix english

* more cleanup

* Temporary Commit at 2/3/2025, 12:24:35 PM

* Temporary Commit at 2/3/2025, 12:39:30 PM

* Temporary Commit at 2/3/2025, 1:43:06 PM
2025-02-03 13:47:16 -08:00
Stanisław Chmiela 2b2b20a0f9 [chore] Remove disabled job (#2853)
It's been failing for a while. I think we'll have renovate some time in universe which will make this obsolete.
2025-02-03 11:24:25 +01:00
Quinlan Jung 77423d1179 [eas-cli] add update group id for fingerprint:compare (#2851)
# Why

Add support for users accidentally passing in update group id into `fingerprint:compare` instead of update id.

# How

Added support for handling update group IDs in the fingerprint comparison command by:
- Attempting to fetch updates using the provided ID as an update group ID
- If successful and there's only one update, using that update directly
- If multiple updates exist in the group, prompting the user to select the specific platform/update
- Falling back to the original update ID logic if the group ID lookup fails
- Adding helpful error messages that include the direct URL to find the correct update ID

# Test Plan

- [ ] `EXPO_STAGING=1 ~/Documents/eas-cli/packages/eas-cli/bin/run fingerprint:compare --update-id ed36a46b-6cc7-4f49-88df-e6ffb66f22d2`
- [ ]  `EXPO_STAGING=1 ~/Documents/eas-cli/packages/eas-cli/bin/run fingerprint:compare --update-id ed36a46b-6cc7-4f49-88df-e6ffb66f22d2 --non-interactive`
2025-01-31 12:25:47 -08:00
Quinlan Jung ebc277dfe4 [eas-cli] add update support for fingerprint:compare (#2850)
# Why

Add support for comparing fingerprints between updates using the `--update-id` flag in the `eas fingerprint compare` command. This allows developers to compare fingerprints between updates, similar to how they can already compare fingerprints between builds.

# How

- Added `updateId` flag to the `FingerprintCompare` command
- Extended the fingerprint comparison logic to handle updates as a fingerprint source
- Added `UpdateQuery.viewByUpdateAsync` to fetch update details including fingerprint information
- Updated the fingerprint origin type system to include updates as a source
- Enhanced the pretty printing of fingerprint sources to show platform information for updates

# Test Plan

- [ ] `EXPO_STAGING=1 ~/Documents/eas-cli/packages/eas-cli/bin/run fingerprint:compare --update-id 087ccb84-5b64-4285-aebf-e890cb95cd20`
2025-01-31 12:24:49 -08:00
Expo CI 427c8f152d update CHANGELOG.md 2025-01-31 16:40:18 +00:00
Stanisław Chmiela 1c245d23b4 v14.7.1 2025-01-31 17:18:56 +01:00
Evan Bacon 29c4695351 Account for ios.config.usesNonExemptEncryption in non-exempt encryption status prompt (#2852) 2025-01-31 16:55:40 +01:00
Expo CI 7b968f6e0d update CHANGELOG.md 2025-01-30 22:47:29 +00:00
Stanisław Chmiela 8a38cc24e1 v14.7.0 2025-01-30 23:42:13 +01:00
Evan Bacon a85b9e2d80 fix: Skip non-exempt check in non-interactive mode. (#2849)
* Skip non-exempt check in non-interactive mode.

* Update CHANGELOG.md
2025-01-30 14:07:21 -08:00
Szymon Dziedzic e0f69327da [eas-cli] add --submit and -s aliases to --auto-submit flag (#2846)
* [eas-cli] add `--submit` and `-s` aliases to `--auto-submit` flag

* changelog
2025-01-30 19:24:53 +01:00
Expo CI 76d4e65d92 update CHANGELOG.md 2025-01-30 17:19:28 +00:00
Szymon Dziedzic 58917ecbb4 v14.6.0 2025-01-30 18:13:53 +01:00
Szymon Dziedzic e75a102361 upgrade eas-cli-local-build-plugin to 1.0.168 2025-01-30 18:13:46 +01:00
Szymon Dziedzic 061f626b2c [ENG-14625][eas-cli] add eas build:dev command (#2820)
* [eas-cli] add eas dev command

* update logs

* rename command do `build:dev`

* filter by dev client

* apply suggested changes

* use real vs virtaul build profiles

* make it hidden

* prompt to commit after adding new build profile
2025-01-30 18:11:37 +01:00
Piotr Szeremeta 3159550468 Move environment from metadata to job object (#2777)
<!-- 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-14307: move environment to job object from metadata](https://linear.app/expo/issue/ENG-14307/move-environment-to-job-object-from-metadata)

`Environment` should be considering  a first class citizen in job object instead of being stored in the metadata.

# How

* Pass `environment` in `job` object
* remove `environment` from `metadata`.

# Test Plan

1. Start a build specifying `environment` in `eas.json`.
2. Build should use the specified environment (check on website in `Build details`)
2025-01-30 10:31:33 +01:00
Quinlan Jung a20bd0051e [eas-cli] release fingerprint:compare (#2821)
* [eas-cli] release fingerprint:compare

* Temporary Commit at 1/15/2025, 11:33:18 PM

* Temporary Commit at 1/28/2025, 4:41:48 PM

* Temporary Commit at 1/28/2025, 5:03:51 PM

* Temporary Commit at 1/28/2025, 5:33:24 PM

* pr feedbak

* Temporary Commit at 1/29/2025, 4:07:55 PM

* fix lint
2025-01-29 20:54:49 -08:00
Evan Bacon 0b64d1c8df feat(eas-cli): Ensure non-exempt encryption status (#2843)
* Ensure non-exempt encryption status

* Update exemptEncryption.ts

* Update CHANGELOG.md

* add tests

* Update exemptEncryption.ts

* Update exemptEncryption.ts

* Update exemptEncryption.ts
2025-01-29 14:04:11 -08:00
Evan Bacon fe37600323 feat: add internal TestFlight group to submit (#2839)
* feat: add internal TestFlight group to submit

* Update CHANGELOG.md

* fixed transient issue

* Update AppProduce.ts

* refactor

* remove ability to add all users

* Update ensureTestFlightGroup.ts

* Update provisioningProfile.ts

* reduce

* Update target-test.ts
2025-01-29 13:44:38 -08:00
Evan Bacon ab507d0bdd feat(eas-cli): Use a fallback generated name during EAS Submit (#2842) 2025-01-28 16:49:35 -08:00
Radosław Krzemień 7505b8d40a [eas-cli] [ENG-14633] Start autoIncremented builds at 1 (#2828)
* [eas-cli] Don't bump default version for Android

When the versionCode is initialized with 1, don't bump it even if autoIncrement=true

See: https://linear.app/expo/issue/ENG-14633/start-autoincrement-counter-at-0-so-first-build-has-buildnumber-==-1

* [eas-cli] Add tests for Android version

Testing the version resolving behavior for Android

See: https://linear.app/expo/issue/ENG-14633/start-autoincrement-counter-at-0-so-first-build-has-buildnumber-==-1

* [eas-cli] Don't bump default version for Ios

When the buildNumber is 1, don't bump it even if autoIncrement=true

See: https://linear.app/expo/issue/ENG-14633/start-autoincrement-counter-at-0-so-first-build-has-buildnumber-==-1

* [eas-cli] Add tests for Ios version

Testing the version resolving behavior for Ios

See: https://linear.app/expo/issue/ENG-14633/start-autoincrement-counter-at-0-so-first-build-has-buildnumber-==-1

* [eas-cli] Lint

Lint

See: https://linear.app/expo/issue/ENG-14633/start-autoincrement-counter-at-0-so-first-build-has-buildnumber-==-1

* [eas-cli] Add tests

Added tests for remote version autoIncrement=true

See: https://linear.app/expo/issue/ENG-14633/start-autoincrement-counter-at-0-so-first-build-has-buildnumber-==-1

* [eas-cli] Add changelog

Updated CHANGELOG.md

See: https://linear.app/expo/issue/ENG-14633/start-autoincrement-counter-at-0-so-first-build-has-buildnumber-==-1
2025-01-28 15:56:40 +01:00
Expo Bot b31ddee6e7 Upgrade eas-build dependencies (#2840) 2025-01-27 13:58:25 +01:00
Expo CI 01ce802b73 update CHANGELOG.md 2025-01-22 13:31:34 +00:00
Radoslaw Krzemien f75ef22154 v14.5.0 2025-01-22 14:24:59 +01:00
Radosław Krzemień 7d3f4e8fd4 [eas-json] [ENG-14636] Allow longer submit profile extension chains (#2831)
* [eas-json] Extend submit profile expansion chain

Extended the limit for expansion chain for submit profiles to equal that of build profiles - 5

See: https://linear.app/expo/issue/ENG-14636/allow-longer-profile-extend-chains-in-easjson

* [eas-json] Add tests for submit profiles

Added tests for submit profiles extension chain

See: https://linear.app/expo/issue/ENG-14636/allow-longer-profile-extend-chains-in-easjson

* [eas-json] Add tests for build profiles

Added tests for build profiles extension chain

See: https://linear.app/expo/issue/ENG-14636/allow-longer-profile-extend-chains-in-easjson

* [eas-json] Lint

Lint

See: https://linear.app/expo/issue/ENG-14636/allow-longer-profile-extend-chains-in-easjson

* [eas-json] Add changelog

Updated CHANGELOG.md

See: https://linear.app/expo/issue/ENG-14636/allow-longer-profile-extend-chains-in-easjson
2025-01-22 14:23:07 +01:00
Radosław Krzemień 004caf4bb7 [eas-cli] [ENG-14629] Improve AppleID validation (#2830)
* [eas-cli] Remove invalid unprintable characters from AppleID

Clears the AppleID received from the prompt

See: https://linear.app/expo/issue/ENG-14629/improve-apple-id-input-validation

* [eas-cli] Add tests

Adds tests for `resolveUserCredentialsAsync` function, including reading malformed AppleID from the prompt

See: https://linear.app/expo/issue/ENG-14629/improve-apple-id-input-validation

* [eas-cli] Remove line

Removed redundant expect()

See: https://linear.app/expo/issue/ENG-14629/improve-apple-id-input-validation

* [eas-cli] Add context

Added a comment with context for the change

See: https://linear.app/expo/issue/ENG-14629/improve-apple-id-input-validation

* [eas-cli] Add changelog

Updated CHANGELOG.md

See: https://linear.app/expo/issue/ENG-14629/improve-apple-id-input-validation
2025-01-22 14:17:14 +01:00
Kadi Kraman 5a16f3d6d8 [eas-cli] Detect valid JSON files during hosting upload (#2829)
* Use application/json as content type for valid JSON without an extension

* Add changelog

* Pull getContentTypeAsync to a separate function
2025-01-21 10:51:32 +00:00
Expo Bot d2e4cb5fc6 Upgrade eas-build dependencies (#2826) 2025-01-18 16:29:59 +01:00
Kadi Kraman 2c1c9b91cb [eas-cli] Use deploy as top level hosting command (#2824)
* Replace preview with help

* Remove redundant alias

* Update description

* Rename top level command from worker -> deploy

* Add changelog

* End sentence with a .

* Add worker:deploy as an alias to deploy
2025-01-17 12:16:02 +00:00
Will Schurman b447194c85 [eas-cli] Make variable naming more explicit, remove deprecated runtimeFingerprintSource uses (#2816) 2025-01-16 08:51:32 -08:00
Kadi Kraman 9ab095597a [eas-cli] fix deploy command typos (#2822)
* EAS hosting is in preview (not beta)

* Remove errant curly

* Add changelog
2025-01-16 13:09:44 +00:00
Szymon Dziedzic b1edff7242 [eas-cli] allow filtering by fingerprint-hash in eas build:list command (#2818)
<!-- 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

Allow filtering by `--fingerprint-hash` in the `eas build:list` command

# How

Update GQL schema to add a new filter field, and add a new flag to use it.

# Test Plan

Test locally
2025-01-16 00:38:54 +01:00
Expo CI 4d9e37269c update CHANGELOG.md 2025-01-15 09:48:15 +00:00
Szymon Dziedzic e7b48949db v14.4.1 2025-01-15 10:43:15 +01:00
Mubin Ansari 1c3edbe492 Bump eas-cli-local-build-plugin to latest version to support new bun lock file (#2817)
* Bump eas-cli-local-build-plugin to latest version to support new bun lock file

* Update changelog
2025-01-15 10:42:38 +01:00
Will Schurman deaa3a9105 [eas-cli] Enhance eas env:exec command by enabling shell execution for commands (#2811) 2025-01-13 09:03:06 -08:00
Quinlan Jung cbfcdaec5d [eas-cli] upload fingerprint from local project state (#2808)
* [eas-cli] upload fingerprint from local project state

* Temporary Commit at 1/10/2025, 8:37:46 AM
2025-01-10 11:38:24 -08:00
Expo Bot 37a32c93ee Upgrade eas-build dependencies (#2810) 2025-01-10 15:46:42 +01:00
Szymon Dziedzic 6461a78fa2 [ENG-14644][eas-cli] make "No remote versions are configured" message green instead of yellow (#2805)
* [eas-cli] make "No remote versions are configured" message green instead of yellow

* add changelog
2025-01-10 11:09:18 +01:00
Szymon Dziedzic 86c8ac093f [ENG-14643][eas-cli] make automatic env resolution message shorter (#2806)
* [eas-cli] make automatic env resolution message shorter

* add changelog entry
2025-01-10 11:03:52 +01:00
Expo CI 608cdff981 update CHANGELOG.md 2025-01-09 16:53:02 +00:00
Phil Pluckthun 4dc4ef85fa v14.4.0 2025-01-09 16:47:28 +00:00
Phil Pluckthun eaf15acb76 feat(eas-cli): Unhide deploy and deploy:alias commands (#2807)
* Unhide deploy command

* Add CHANGELOG entry
2025-01-09 12:47:46 +00:00
Will Schurman ae4ca91658 [eas-cli] Silence all non-command output in non-interactive mode of eas env:exec (#2800) 2025-01-08 09:09:23 -08:00
Phil PluckthunandKadi Kraman 23ebb77952 [eas-cli] Combine .env and EAS environment variables for deploy command (#2783)
* Allow env vars to be loaded from .env and EAS env simultaneously

* Add CHANGELOG entry

* Apply lints

* Switch to new @expo/[email protected] API

* Update packages/eas-cli/src/commands/worker/deploy.ts

Co-authored-by: Kadi Kraman <[email protected]>

---------

Co-authored-by: Kadi Kraman <[email protected]>
2025-01-08 17:02:27 +00:00
Expo CI ed2352f289 update CHANGELOG.md 2025-01-08 13:53:33 +00:00
Szymon Dziedzic 38ae8b68f1 v14.3.1 2025-01-08 14:48:06 +01:00
Kudo Chien 5e02807ac9 [eas-cli] bump package-manager version (#2801)
# Why

fixes https://github.com/expo/eas-cli/issues/2782

# How

`@expo/[email protected]` supports bun text-based lock file. this pr bumps the version
2025-01-08 21:44:38 +08:00
Piotr Szeremeta f05d6ca3a2 ENG-14518 Add keystoreName field to build profile (#2775)
<!-- 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-14518: Add `keystoreName` field to build profile to allow selecting/creating named keystore to use for a build](https://linear.app/expo/issue/ENG-14518/add-keystorename-field-to-build-profile-to-allow-selectingcreating)

Allow specifying keystore name of credentials to be used with a build.

# How

* Added `keystoreName` to eas.json schema
* Pass `name` option to `AndroidCredentialsProvider`

# Test Plan

Added test, tested manually.

1. Create a new android credentials with `eas credentials`
2. Specify `keystoreName` in build profile in `eas.json`
3. Ensure that it works only for android builds
4. Ensure it works only for `remote` `credentialsSource`
5. Ensure that the correct credentials are used

<img width="326" alt="image" src="https://github.com/user-attachments/assets/1245c459-5fa1-4253-b5fc-bdf7a68b814a" />

<img width="628" alt="image" src="https://github.com/user-attachments/assets/5bca4cf7-c895-462c-b527-ef37ce659aca" />
2025-01-07 13:07:47 +01:00
Expo CI b57082647d update CHANGELOG.md 2025-01-07 10:50:10 +00:00
Stanisław Chmiela 9d4fdc3c75 v14.3.0 2025-01-07 11:44:47 +01:00
Stanisław Chmiela d1af9e385f [eas-cli] Upload package.json alongside eas.json when creating a workflow run (#2786)
<!-- 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

Thanks to @szdziedzic's changes to `www`, if we send a `package.json` along with `eas.json` when setting up a workflow run, EAS will pick the right images based on SDK and RN versions as `eas build` would.

# How

Added uploading `package.json` after uploading `eas.json`, added the new argument to the mutation.

# Test Plan

Tested manually.

<img width="874" alt="Zrzut ekranu 2024-12-20 o 22 27 20" src="https://github.com/user-attachments/assets/383c4a19-17a1-4f46-8113-bc6dc8aaba2e" />
<img width="833" alt="Zrzut ekranu 2024-12-20 o 22 27 58" src="https://github.com/user-attachments/assets/be9e2396-0779-4043-925f-d784b5168764" />
2025-01-07 11:05:46 +01:00
Quinlan Jung 59bbe19dde [eas-cli][eas-json] use node18 as tsconfig base (#2739)
* [eas-cli][eas-json] use node18 as tsconfig base

* Temporary Commit at 1/6/2025, 9:06:36 AM

* Temporary Commit at 1/6/2025, 9:17:06 AM
2025-01-06 09:44:24 -08:00
Bartosz Kaszubowski 5e5b55f9e4 [eas-cli] wording tweaks for messages (#2790) 2025-01-02 15:40:00 +01:00
Kadi Kraman a1d086ffc6 [eas-cli] parse request body for error only when needed (#2784)
* Parse error only when needed

* Add changelog

* Fix linting
2024-12-20 15:36:39 +00:00
Phil Pluckthun 2a96d04be3 [eas-cli] Update deploy/alias log output copy (#2780)
* Update product name

* Highlight `Project export` text

* Update export copy

* Update product name for alias

* Add changelog entry
2024-12-19 13:04:33 +00:00
Phil Pluckthun d126ee988a [eas-cli] Prevent eas-cli dependencies check from running repeatedly (#2781)
* Only check for dependencies once

* Add CHANGELOG entry

* Apply lints
2024-12-19 13:03:58 +00:00
Expo Bot 4bb550e3bb Upgrade eas-build dependencies (#2776) 2024-12-18 13:12:27 +01:00
Kadi Kraman 6a55c65908 [eas-cli] show deployment errors (#2771)
* Show upload errors

* Add changelog

* Fix changelog

* Show error from request body when applicable
2024-12-13 17:08:53 +00:00
Expo CI ab3495da5e update CHANGELOG.md 2024-12-13 15:25:41 +00:00
Szymon Dziedzic 63b67635e0 v14.2.0 2024-12-13 16:20:07 +01:00
Szymon Dziedzic 4b5cda3bbc [eas-cli] fix request for switching providers (#2769)
* [eas-cli] fix request for switching providers

* add changelog entry
2024-12-13 16:19:04 +01:00
Szymon Dziedzic 63e3c1f2ab [eas-cli] improvements to eas env:exec command (#2762)
* [eas-cli] improvements to eas env:exec command

* add changelog
2024-12-13 16:05:02 +01:00
Expo Bot 3f1543989f Upgrade eas-build dependencies (#2768) 2024-12-13 16:04:25 +01:00
Piotr Szeremeta 1c964d2ab1 [ENG-14328] Pass environment to PublishMutation.publishUpdateGroupAsync (#2757)
<!-- 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-14328: store `environment` info on update entity](https://linear.app/expo/issue/ENG-14328/store-environment-info-on-update-entity)

Pass `environment` to `PublishMutation.publishUpdateGroupAsync` to store in the db.

Waits for changes on the `www`. https://github.com/expo/universe/pull/17921
2024-12-13 10:41:08 +01:00
243 changed files with 22206 additions and 4681 deletions

No files matched your search

+5 -1
View File
@@ -3,7 +3,7 @@ commentConfiguration:
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela', 'radoslawkrzemien']
mentions: ['sjchmiela']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/update/**']
@@ -18,3 +18,7 @@ rules:
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/commands/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/worker/**']
mentions: ['kitten', 'kadikraman']
- patterns: ['packages/eas-cli/src/commands/deploy/**']
mentions: ['kitten', 'kadikraman']
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v2
- name: Check if CHANGELOG.md was updated
id: changelog-updated
uses: tj-actions/changed-files@v41
uses: tj-actions/changed-files@b1ba699b304f2083b602164e06a89b868c84f076
with:
files: CHANGELOG.md
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
+5 -13
View File
@@ -18,6 +18,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: 'eas-cli@${{ env.EAS_CLI_VERSION }}'
draft: true
build-linux:
name: Build for Linux
@@ -30,6 +31,10 @@ jobs:
node-version: 20
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Ensure GraphQL schema and generated code is up-to-date
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn verify-graphql-code
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
@@ -226,16 +231,3 @@ jobs:
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
open-pr-in-universe:
name: Open a PR with `@expo/eas-json` bump in universe
needs: publish-to-npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Open a PR with `@expo/eas-json` version bump in universe
uses: benc-uk/workflow-dispatch@v1
with:
repo: expo/universe
ref: main
workflow: update-eas-json.yml
token: ${{ secrets.EXPO_BOT_PAT }}
+30
View File
@@ -0,0 +1,30 @@
name: Ensure GraphQL schema and generated code is up-to-date
on:
push:
branches: [main]
pull_request:
branches: [main]
paths:
- .github/workflows/test-graphql.yml
- packages/eas-cli/src/**
schedule:
- cron: '0 8 * * *' # 8:00 AM UTC time every day
concurrency:
group: test-graphql-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Ensure GraphQL schema and generated code is up-to-date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 22
- run: yarn install --frozen-lockfile
- run: yarn verify-graphql-code
working-directory: packages/eas-cli
+4 -1
View File
@@ -28,6 +28,9 @@ jobs:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck
- run: |
git config --global user.email "[email protected]"
git config --global user.name "Test"
- run: yarn test
if: ${{ !matrix.coverage }}
env:
@@ -49,7 +52,7 @@ jobs:
notify-slack:
runs-on: ubuntu-latest
needs: test
needs: [test]
name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }}
steps:
@@ -66,7 +66,7 @@ jobs:
destination_branch: 'main'
pr_title: 'Upgrade eas-build dependencies'
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
pr_reviewer: 'szdziedzic,radoslawkrzemien,sjchmiela,khamilowicz'
pr_reviewer: 'sjchmiela'
github_token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/github-script@v3
+509
View File
@@ -8,16 +8,525 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add `deploy:delete` for deleting worker deployments ([#3106](https://github.com/expo/eas-cli/pull/3106) by [@kadikraman](https://github.com/kadikraman))
- Add `deploy:alias:delete` for deleting worker deployment aliases ([#3107](https://github.com/expo/eas-cli/pull/3107) by [@kadikraman](https://github.com/kadikraman))
### 🐛 Bug fixes
### 🧹 Chores
## [16.16.0](https://github.com/expo/eas-cli/releases/tag/v16.16.0) - 2025-07-16
### 🎉 New features
- Add workflow:view and workflow:logs commands. ([#3090](https://github.com/expo/eas-cli/pull/3090) by [@douglowder](https://github.com/douglowder))
- Add support for providing inputs to new workflow runs ([#3095](https://github.com/expo/eas-cli/pull/3095) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- Refactor capabilities syncing to avoid provisioning profiles becoming invalid. ([#3088](https://github.com/expo/eas-cli/pull/3088) by [@vonovak](https://github.com/vonovak))
- Avoid corrupted EAS Hosting batch upload bodies and double-check length and checksums while uploading. ([#3103](https://github.com/expo/eas-cli/pull/3103) by [@kitten](https://github.com/kitten))
## [16.15.0](https://github.com/expo/eas-cli/releases/tag/v16.15.0) - 2025-07-10
### 🎉 New features
- Implement new EAS Hosting asset upload protocol, which improves performance by grouping files into batched upload API calls ([#3085](https://github.com/expo/eas-cli/pull/3085) by [@kitten](https://github.com/kitten))
### 🧹 Chores
- Update the EAS Hosting worker deployment progress indicator to use a progress bar for asset uploads instead ([#3099](https://github.com/expo/eas-cli/pull/3099) by [@kitten](https://github.com/kitten))
## [16.14.1](https://github.com/expo/eas-cli/releases/tag/v16.14.1) - 2025-07-08
### 🧹 Chores
- Update apple team info as needed. ([#3096](https://github.com/expo/eas-cli/pull/3096) by [@quinlanj](https://github.com/quinlanj))
## [16.14.0](https://github.com/expo/eas-cli/releases/tag/v16.14.0) - 2025-07-07
### 🎉 New features
- Add update:revert-update-rollout command. ([#3068](https://github.com/expo/eas-cli/pull/3068) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove unreleased `link` and `unlink` commands from `eas env`. ([#3092](https://github.com/expo/eas-cli/pull/3092) by [@kadikraman](https://github.com/kadikraman))
- Add new CI to verify GraphQL generation. ([#3093](https://github.com/expo/eas-cli/pull/3093) by [@douglowder](https://github.com/douglowder))
- Update GraphQL generation to look in all src directories. ([#3097](https://github.com/expo/eas-cli/pull/3097) by [@wschurman](https://github.com/wschurman))
## [16.13.4](https://github.com/expo/eas-cli/releases/tag/v16.13.4) - 2025-07-04
### 🧹 Chores
- Remove hidden flag from `eas upload`. ([#3014](https://github.com/expo/eas-cli/pull/3014) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.13.3](https://github.com/expo/eas-cli/releases/tag/v16.13.3) - 2025-07-01
### 🐛 Bug fixes
- Fix suggested command to switch Xcode Developer Tools. ([#3079](https://github.com/expo/eas-cli/pull/3079) by [@rolfb](https://github.com/rolfb))
- Skip configuring build profiles when running `eas update:configure` if no build profiles exist. ([#3084](https://github.com/expo/eas-cli/pull/3084) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Reword how paid plans are described ([#3082](https://github.com/expo/eas-cli/pull/3082) by [@ide](https://github.com/ide))
## [16.13.2](https://github.com/expo/eas-cli/releases/tag/v16.13.2) - 2025-06-27
### 🧹 Chores
- Amend update override fields. ([#3080](https://github.com/expo/eas-cli/pull/3080) by [@quinlanj](https://github.com/quinlanj))
## [16.13.1](https://github.com/expo/eas-cli/releases/tag/v16.13.1) - 2025-06-25
### 🐛 Bug fixes
- Fix bug in log during update:republish. ([#3067](https://github.com/expo/eas-cli/pull/3067) by [@wschurman](https://github.com/wschurman))
- Fix update group deletion. ([#3069](https://github.com/expo/eas-cli/pull/3069) by [@wschurman](https://github.com/wschurman))
## [16.13.0](https://github.com/expo/eas-cli/releases/tag/v16.13.0) - 2025-06-24
### 🎉 New features
- Add `--what-to-test` option to `build` command. ([#3065](https://github.com/expo/eas-cli/pull/3065) by [@vonovak](https://github.com/vonovak))
### 🐛 Bug fixes
- Hide `workflow:list`. ([#3060](https://github.com/expo/eas-cli/pull/3060) by [@douglowder](https://github.com/douglowder))
- Align text of messages from `build` and `workflow:run`. ([#3061](https://github.com/expo/eas-cli/pull/3061) by [@douglowder](https://github.com/douglowder))
## [16.12.0](https://github.com/expo/eas-cli/releases/tag/v16.12.0) - 2025-06-20
### 🎉 New features
- Add `--what-to-test` option to the `submit` command. ([#3023](https://github.com/expo/eas-cli/pull/3023) by [@vonovak](https://github.com/vonovak))
### 🐛 Bug fixes
- Fixed monorepo support in `workflow:run` if a project is not connected to a GitHub repository. ([#3058](https://github.com/expo/eas-cli/pull/3058) by [@sjchmiela](https://github.com/sjchmiela))
## [16.10.1](https://github.com/expo/eas-cli/releases/tag/v16.10.1) - 2025-06-13
### 🐛 Bug fixes
- Fixed `eas build:dev` command to correctly use passed build profile. ([#3053](https://github.com/expo/eas-cli/pull/3053) by [@sebryu](https://github.com/sebryu))
### 🧹 Chores
- Allow rolling out update on empty branch. ([#3050](https://github.com/expo/eas-cli/pull/3050) by [@wschurman](https://github.com/wschurman))
## [16.10.0](https://github.com/expo/eas-cli/releases/tag/v16.10.0) - 2025-06-12
### 🎉 New features
- [eas-cli] Add workflow:cancel. ([#3048](https://github.com/expo/eas-cli/pull/3048) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- Removed repack support. ([#3044](https://github.com/expo/eas-cli/pull/3044) by [@kudo](https://github.com/kudo))
## [16.9.0](https://github.com/expo/eas-cli/releases/tag/v16.9.0) - 2025-06-06
### 🎉 New features
- Add `groups` option to the `submit` command. ([#2891](https://github.com/expo/eas-cli/pull/2891) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
- [eas-cli] Ensure workflow:runs returns most recent runs. ([#3045](https://github.com/expo/eas-cli/pull/3045) by [@douglowder](https://github.com/douglowder))
## [16.8.0](https://github.com/expo/eas-cli/releases/tag/v16.8.0) - 2025-06-04
### 🎉 New features
- [eas-cli] Add workflow:list and workflow:runs. ([#3030](https://github.com/expo/eas-cli/pull/3030) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- Change location of EAS Update host override config. ([#3042](https://github.com/expo/eas-cli/pull/3042) by [@wschurman](https://github.com/wschurman))
## [16.7.2](https://github.com/expo/eas-cli/releases/tag/v16.7.2) - 2025-06-03
### 🐛 Bug fixes
- Add support for array `flavorDimensions` (`flavorDimensions = ['version']`). ([#3043](https://github.com/expo/eas-cli/pull/3043) by [@sjchmiela](https://github.com/sjchmiela))
## [16.7.1](https://github.com/expo/eas-cli/releases/tag/v16.7.1) - 2025-06-03
### 🐛 Bug fixes
- Fix some unit tests by updating mocks. ([#3035](https://github.com/expo/eas-cli/pull/3035) by [@douglowder](https://github.com/douglowder))
- Fix update republishing not including environment or git info. ([#3036](https://github.com/expo/eas-cli/pull/3036) by [@wschurman](https://github.com/wschurman))
- Do not require `eas.json` or `package.json` files when creating new workflow runs. ([#3028](https://github.com/expo/eas-cli/pull/3028) by [@sjchmiela](https://github.com/sjchmiela))
- Add `EAS_SKIP_CLI_VERSION_CHECK` allowing us to skip `eas-cli` version check (against `eas.json#version`). ([#3041](https://github.com/expo/eas-cli/pull/3041) by [@sjchmiela](https://github.com/sjchmiela))
## [16.7.0](https://github.com/expo/eas-cli/releases/tag/v16.7.0) - 2025-05-29
### 🎉 New features
- Add support for manifest host and asset host overriding for EAS Update. ([#3021](https://github.com/expo/eas-cli/pull/3021) by [@wschurman](https://github.com/wschurman))
- Add rollout flag to update:republish. ([#3029](https://github.com/expo/eas-cli/pull/3029) by [@quinlanj](https://github.com/quinlanj))
## [16.6.2](https://github.com/expo/eas-cli/releases/tag/v16.6.2) - 2025-05-23
### 🧹 Chores
- Further no longer require owner field for SDK >= 53 or canary. ([#3017](https://github.com/expo/eas-cli/pull/3017) by [@wschurman](https://github.com/wschurman))
## [16.6.1](https://github.com/expo/eas-cli/releases/tag/v16.6.1) - 2025-05-14
### 🐛 Bug fixes
- Onboarding: add cwd to spawned git process. ([#3015](https://github.com/expo/eas-cli/pull/3015) by [@quinlanj](https://github.com/quinlanj))
## [16.6.0](https://github.com/expo/eas-cli/releases/tag/v16.6.0) - 2025-05-14
### 🎉 New features
- Add support for uploading iOS internal distribution local builds. ([#3014](https://github.com/expo/eas-cli/pull/3014) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.5.0](https://github.com/expo/eas-cli/releases/tag/v16.5.0) - 2025-05-13
### 🎉 New features
- Add `--wait`/`--no-wait` flag to `eas workflow:run`. ([#3012](https://github.com/expo/eas-cli/pull/3012) by [@sjchmiela](https://github.com/sjchmiela))
- Add `--json` flag to `eas workflow:run`. ([#3013](https://github.com/expo/eas-cli/pull/3013) by [@sjchmiela](https://github.com/sjchmiela))
## [16.4.2](https://github.com/expo/eas-cli/releases/tag/v16.4.2) - 2025-05-10
### 🧹 Chores
- Update `eas-build-cache-provider` function names to use `buildCacheProvider` key. ([#3002](https://github.com/expo/eas-cli/pull/3002) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.4.1](https://github.com/expo/eas-cli/releases/tag/v16.4.1) - 2025-05-07
### 🧹 Chores
- Allow non-standard build types in custom gradle command. ([#3003](https://github.com/expo/eas-cli/pull/3003) by [@khamilowicz](https://github.com/khamilowicz))
## [16.4.0](https://github.com/expo/eas-cli/releases/tag/v16.4.0) - 2025-05-02
### 🎉 New features
- Add `--verbose-logs` flag for `build` command ([#3000](https://github.com/expo/eas-cli/pull/3000) by [@khamilowicz](https://github.com/khamilowicz))
- Add `eas-build-cache-provider` package. ([#3002](https://github.com/expo/eas-cli/pull/3002) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 🧹 Chores
- Read remote build cache provider types from `@expo/config`. ([#3005](https://github.com/expo/eas-cli/pull/3005) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.3.3](https://github.com/expo/eas-cli/releases/tag/v16.3.3) - 2025-04-24
### 🐛 Bug fixes
- Add `sdk-52` alias to the list or supported android images. ([#2989](https://github.com/expo/eas-cli/pull/2989) by [@kadikraman](https://github.com/kadikraman))
- Detect when expo metro config is used. ([#2996](https://github.com/expo/eas-cli/pull/2996) by [@kadikraman](https://github.com/kadikraman))
### 🧹 Chores
- upload assetmap.json when publishing update. ([#2994](https://github.com/expo/eas-cli/pull/2994) by [@quinlanj](https://github.com/quinlanj))
- Add error messages for CDN-level upload errors. ([#2998](https://github.com/expo/eas-cli/pull/2998) by [@kitten](https://github.com/kitten))
## [16.3.2](https://github.com/expo/eas-cli/releases/tag/v16.3.2) - 2025-04-17
### 🧹 Chores
- Update upload command to display build info. ([#2990](https://github.com/expo/eas-cli/pull/2990) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- change launch asset file extension. ([#2991](https://github.com/expo/eas-cli/pull/2991) by [@quinlanj](https://github.com/quinlanj))
## [16.3.1](https://github.com/expo/eas-cli/releases/tag/v16.3.1) - 2025-04-11
### 🧹 Chores
- Add `--no-dev-client` flag for `build:download` command. ([#2985](https://github.com/expo/eas-cli/pull/2985) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.3.0](https://github.com/expo/eas-cli/releases/tag/v16.3.0) - 2025-04-09
### 🎉 New features
- Add `eas upload` command. ([#2932](https://github.com/expo/eas-cli/pull/2932), [#2981](https://github.com/expo/eas-cli/pull/2981), [#2983](https://github.com/expo/eas-cli/pull/2983) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Add `eas build:download` command. ([#2982](https://github.com/expo/eas-cli/pull/2982) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.2.2](https://github.com/expo/eas-cli/releases/tag/v16.2.2) - 2025-04-08
## [16.2.1](https://github.com/expo/eas-cli/releases/tag/v16.2.1) - 2025-04-04
### 🧹 Chores
- Bump `@expo/apple-utils` to use async JWT API. ([#2973](https://github.com/expo/eas-cli/pull/2973) by [@EvanBacon](https://github.com/EvanBacon))
## [16.2.0](https://github.com/expo/eas-cli/releases/tag/v16.2.0) - 2025-04-03
### 🎉 New features
- Add environment flag to `eas fingerprint:compare`. ([#2954](https://github.com/expo/eas-cli/pull/2954) by [@quinlanj](https://github.com/quinlanj))
- Add build-profile flag to `eas fingerprint:generate`. ([#2966](https://github.com/expo/eas-cli/pull/2966) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Remove hidden flag from `eas fingerprint:generate`. ([#2965](https://github.com/expo/eas-cli/pull/2965) by [@quinlanj](https://github.com/quinlanj))
- Refactor `eas update` command to improve code readability. ([#2976](https://github.com/expo/eas-cli/pull/2976) by [@quinlanj](https://github.com/quinlanj))
- `eas update`: add warning if no build exists with fingerprint. ([#2977](https://github.com/expo/eas-cli/pull/2977) by [@quinlanj](https://github.com/quinlanj))
## [16.1.0](https://github.com/expo/eas-cli/releases/tag/v16.1.0) - 2025-03-26
### 🎉 New features
- Add environment flag to `eas fingerprint:generate`. ([#2951](https://github.com/expo/eas-cli/pull/2951) by [@quinlanj](https://github.com/quinlanj))
- Add `corepack` field to `eas.json`. ([#2964](https://github.com/expo/eas-cli/pull/2964) by [@szdziedzic](https://github.com/szdziedzic))
## [16.0.1](https://github.com/expo/eas-cli/releases/tag/v16.0.1) - 2025-03-20
### 🐛 Bug fixes
- Make `eas update:configure` re-apply configuration from app.json /app.config.js when run multiple times. ([#2957](https://github.com/expo/eas-cli/pull/2957) by [@brentvatne](https://github.com/brentvatne))
## [16.0.0](https://github.com/expo/eas-cli/releases/tag/v16.0.0) - 2025-03-19
### 🛠 Breaking changes
- Add support for `.easignore` when `requireCommit` is set to `true`. ([#2942](https://github.com/expo/eas-cli/pull/2942) by [@sjchmiela](https://github.com/sjchmiela))
- Up to 15.0.0, if `requireCommit` was `true`, `.easignore` was silently ignored.
- Versions 15.0.0-15.0.13 started using `.easignore` to skip files from being bundled into a tarball when `requireCommit` was `true`. This was an unexpected change in behavior.
- To clear this up, versions 15.0.13-15.0.15 were erroring if `.easignore` was present when `requireCommit` was `true`.
- `[email protected]` formalizes the 15.0.0-15.0.13 behavior by adhering to `.easignore` even when `requireCommit` is set to `true`.
- If you know what you're doing and you want to suppress a warning printed, you can do so by setting `EAS_SUPPRESS_REQUIRE_COMMIT_EASIGNORE_WARNING` environment variable to `true`.
### 🎉 New features
- Add requestId to ApiV2Error. ([#2941](https://github.com/expo/eas-cli/pull/2941) by [@wschurman](https://github.com/wschurman))
- Release `eas fingerprint:generate` in hidden mode ([#2937](https://github.com/expo/eas-cli/pull/2937) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Use correct logic to determine whether artifacts have expired in `eas build:run` command. ([#2931](https://github.com/expo/eas-cli/pull/2931) by [@szdziedzic](https://github.com/szdziedzic))
## [15.0.15](https://github.com/expo/eas-cli/releases/tag/v15.0.15) - 2025-03-12
### 🐛 Bug fixes
- Pass through the updates version to `setUpdatesConfigAsync`, which expects it in order to determine which field values to use. ([#2934](https://github.com/expo/eas-cli/pull/2934) by [@brentvatne](https://github.com/brentvatne)).
## [15.0.14](https://github.com/expo/eas-cli/releases/tag/v15.0.14) - 2025-03-06
### 🐛 Bug fixes
- In `EAS_NO_VCS=1`, use Git for repository root when `EAS_PROJECT_ROOT` is not set. ([#2901](https://github.com/expo/eas-cli/pull/2901) by [@sjchmiela](https://github.com/sjchmiela))
### 🧹 Chores
- Suggest using `eas build:dev` for matching configurations. ([#2929](https://github.com/expo/eas-cli/pull/2929) by [@szdziedzic](https://github.com/szdziedzic))
## [15.0.13](https://github.com/expo/eas-cli/releases/tag/v15.0.13) - 2025-03-04
### 🐛 Bug fixes
- Fixed `.git` being always unexpectedly removed if you had `requireCommit: true` and `.easignore` present. ([#2925](https://github.com/expo/eas-cli/pull/2925) by [@sjchmiela](https://github.com/sjchmiela))
### 🧹 Chores
- Fix `eas fingerprint:compare` URL generation and pretty prints. ([#2909](https://github.com/expo/eas-cli/pull/2909) by [@quinlanj](https://github.com/quinlanj))
- fix formatFields to handle empty array. ([#2914](https://github.com/expo/eas-cli/pull/2914) by [@quinlanj](https://github.com/quinlanj))
- fix git diff header in `fingerprint:compare`. ([#2915](https://github.com/expo/eas-cli/pull/2915) by [@quinlanj](https://github.com/quinlanj))
- amend fingerprint compare url. ([#2923](https://github.com/expo/eas-cli/pull/2923) by [@quinlanj](https://github.com/quinlanj))
- improve error message for `applicationIdSuffix`. ([#2924](https://github.com/expo/eas-cli/pull/2924) by [@kadikraman](https://github.com/kadikraman))
## [15.0.12](https://github.com/expo/eas-cli/releases/tag/v15.0.12) - 2025-02-22
### 🐛 Bug fixes
- Fix APNS key creation. ([#2916](https://github.com/expo/eas-cli/pull/2916) by [@EvanBacon](https://github.com/EvanBacon))
## [15.0.11](https://github.com/expo/eas-cli/releases/tag/v15.0.11) - 2025-02-21
### 🐛 Bug fixes
- Fix internal TestFlight group creation. ([#2906](https://github.com/expo/eas-cli/pull/2906) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Narrow amount of data queried for basic update channel operations. ([#2901](https://github.com/expo/eas-cli/pull/2901) by [@wschurman](https://github.com/wschurman))
- Fix `eas fingerprint:compare` description. ([#2908](https://github.com/expo/eas-cli/pull/2908) by [@quinlanj](https://github.com/quinlanj))
- Skip auto-creation of TestFlight group when there are already exisitng TestFlight groups and allow to opt out of the behavior by setting `EAS_NO_AUTO_TESTFLIGHT_SETUP` env var. ([#2856](https://github.com/expo/eas-cli/pull/2856) by [@szdziedzic](https://github.com/szdziedzic))
## [15.0.10](https://github.com/expo/eas-cli/releases/tag/v15.0.10) - 2025-02-11
### 🐛 Bug fixes
- Fix files deleted in working directory not being removed from the project archive when `requireCommit` is false. ([#2900](https://github.com/expo/eas-cli/pull/2900) by [@sjchmiela](https://github.com/sjchmiela))
## [15.0.9](https://github.com/expo/eas-cli/releases/tag/v15.0.9) - 2025-02-09
### 🐛 Bug fixes
- Fix files not being ignored when creating a tarball on Windows in Git repository in no `requireCommit` mode. ([#2894](https://github.com/expo/eas-cli/pull/2894) by [@sjchmiela](https://github.com/sjchmiela))
## [15.0.8](https://github.com/expo/eas-cli/releases/tag/v15.0.8) - 2025-02-09
## [15.0.7](https://github.com/expo/eas-cli/releases/tag/v15.0.7) - 2025-02-09
## [15.0.6](https://github.com/expo/eas-cli/releases/tag/v15.0.6) - 2025-02-07
## [15.0.5](https://github.com/expo/eas-cli/releases/tag/v15.0.5) - 2025-02-06
### 🐛 Bug fixes
- Do not copy files over onto a cloned Git repository when packing the project archive if `requireCommit` is true. ([#2885](https://github.com/expo/eas-cli/pull/2885) by [@sjchmiela](https://github.com/sjchmiela))
- Fix `EISDIR` error when archiving project with submodules ignored. ([#2884](https://github.com/expo/eas-cli/pull/2884) by [@sjchmiela](https://github.com/sjchmiela))
## [15.0.4](https://github.com/expo/eas-cli/releases/tag/v15.0.4) - 2025-02-05
### 🐛 Bug fixes
- Fixed `GitClient` not respecting `.easignore` file. ([#2873](https://github.com/expo/eas-cli/pull/2873) by [@sjchmiela](https://github.com/sjchmiela))
- Fix symlink support in `makeShallowCopyAsync`. ([#2874](https://github.com/expo/eas-cli/pull/2874) by [@sjchmiela](https://github.com/sjchmiela))
- Allow excluding `.git` directory from project archive by adding it to `.easignore`. ([#2879](https://github.com/expo/eas-cli/pull/2879) by [@sjchmiela](https://github.com/sjchmiela))
### 🧹 Chores
- Popup website in fingerprint:compare. ([#2859](https://github.com/expo/eas-cli/pull/2859) by [@quinlanj](https://github.com/quinlanj))
- Fix fingerprint:compare URL. ([#2861](https://github.com/expo/eas-cli/pull/2861) by [@quinlanj](https://github.com/quinlanj))
- Make less gql calls in fingerprint:compare. ([#2860](https://github.com/expo/eas-cli/pull/2860) by [@quinlanj](https://github.com/quinlanj))
- No longer require owner field for SDK >= 53 or canary. ([#2835](https://github.com/expo/eas-cli/pull/2835) by [@wschurman](https://github.com/wschurman))
- Add --open flag to fingerprint:compare. ([#2872](https://github.com/expo/eas-cli/pull/2872) by [@quinlanj](https://github.com/quinlanj))
## [15.0.3](https://github.com/expo/eas-cli/releases/tag/v15.0.3) - 2025-02-04
### 🐛 Bug fixes
- Fixed EAS server environment variables does not pass to `npx expo-updates runtimeversion:resolve` call. ([#2867](https://github.com/expo/eas-cli/pull/2867) by [@kudo](https://github.com/kudo))
## [15.0.2](https://github.com/expo/eas-cli/releases/tag/v15.0.2) - 2025-02-04
## [15.0.1](https://github.com/expo/eas-cli/releases/tag/v15.0.1) - 2025-02-04
## [15.0.0](https://github.com/expo/eas-cli/releases/tag/v15.0.0) - 2025-02-04
### 🛠 Breaking changes
- Use Git to archive projects containing a Git repository. (Previously, Git would only be used if `requireCommit` flag in `eas.json` was set to `true`.) ([#2841](https://github.com/expo/eas-cli/pull/2841) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- Print warning for `NoVcsClient` only once. ([#2863](https://github.com/expo/eas-cli/pull/2863) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Add update support for fingerprint:compare. ([#2850](https://github.com/expo/eas-cli/pull/2850) by [@quinlanj](https://github.com/quinlanj))
- Add update group id support for fingerprint:compare. ([#2851](https://github.com/expo/eas-cli/pull/2851) by [@quinlanj](https://github.com/quinlanj))
- Add better interactive support for fingerprint:compare. ([#2854](https://github.com/expo/eas-cli/pull/2854) by [@quinlanj](https://github.com/quinlanj))
## [14.7.1](https://github.com/expo/eas-cli/releases/tag/v14.7.1) - 2025-01-31
### 🐛 Bug fixes
- Account for `ios.config.usesNonExemptEncryption` in non-exempt encryption status prompt. ([#2852](https://github.com/expo/eas-cli/pull/2852) by [@EvanBacon](https://github.com/EvanBacon))
## [14.7.0](https://github.com/expo/eas-cli/releases/tag/v14.7.0) - 2025-01-30
### 🎉 New features
- Add `--submit` and `-s` as aliases for `--auto-submit` flag. ([#2846](https://github.com/expo/eas-cli/pull/2846) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Skip non-exempt check in non-interactive mode. ([#2849](https://github.com/expo/eas-cli/pull/2849) by [@EvanBacon](https://github.com/EvanBacon))
## [14.6.0](https://github.com/expo/eas-cli/releases/tag/v14.6.0) - 2025-01-30
### 🎉 New features
- Prompt to set non-exempt encryption status for the iOS app to support faster store submissions. ([#2843](https://github.com/expo/eas-cli/pull/2843) by [@EvanBacon](https://github.com/EvanBacon))
- Automatically create internal TestFlight group in EAS Submit command. ([#2839](https://github.com/expo/eas-cli/pull/2839) by [@evanbacon](https://github.com/evanbacon))
- Sanitize and generate names for EAS Submit to prevent failures due to invalid characters or taken names. ([#2842](https://github.com/expo/eas-cli/pull/2842) by [@evanbacon](https://github.com/evanbacon))
- Release `eas fingerprint:compare`. ([#2821](https://github.com/expo/eas-cli/pull/2821) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Make new autoIncremented builds start at nr 1 by default ([#2828](https://github.com/expo/eas-cli/pull/2828) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
## [14.5.0](https://github.com/expo/eas-cli/releases/tag/v14.5.0) - 2025-01-22
### 🎉 New features
- Allow filtering by `--fingerprint_hash` in `eas build:list` command. ([#2818](https://github.com/expo/eas-cli/pull/2818) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Ensure that the AASA file is served with content type application/json ([#2829](https://github.com/expo/eas-cli/pull/2829) by [@kadikraman](https://github.com/kadikraman))
- Ensure that the AppleID provided in prompt or saved to cache does not contain invalid unprintable characters ([#2830](https://github.com/expo/eas-cli/pull/2830) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
### 🧹 Chores
- Fix logs typos in the `eas deploy` command. ([#2822](https://github.com/expo/eas-cli/pull/2822) by [@kadikraman](https://github.com/kadikraman))
- Make `deploy` the top level command for hosting. ([#2824](https://github.com/expo/eas-cli/pull/2824) by [@kadikraman](https://github.com/kadikraman))
- Allow longer submit profile extension chain (up to 5, same as build profile) ([#2831](https://github.com/expo/eas-cli/pull/2831) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Make variable naming more explicit, remove deprecated runtimeFingerprintSource uses. ([#2816](https://github.com/expo/eas-cli/pull/2816) by [@wschurman](https://github.com/wschurman))
## [14.4.1](https://github.com/expo/eas-cli/releases/tag/v14.4.1) - 2025-01-15
### 🐛 Bug fixes
- Enable shell execution for env:exec commands. ([#2788](https://github.com/expo/eas-cli/pull/2788) by [@tharakadesilva](https://github.com/tharakadesilva))
### 🧹 Chores
- Make automatic env resolution message shorter. ([#2806](https://github.com/expo/eas-cli/pull/2806) by [@szdziedzic](https://github.com/szdziedzic))
- Make "No remote versions are configured" message green instead of yellow. ([#2805](https://github.com/expo/eas-cli/pull/2805) by [@szdziedzic](https://github.com/szdziedzic))
- Upload local fingerprint on `eas fingerprint:compare`. ([#2808](https://github.com/expo/eas-cli/pull/2808) by [@quinlanj](https://github.com/quinlanj))
- Upgrade `eas-cli-local-build-plugin` to `1.0.163` to support Bun's new text-based lock file in local builds. ([#2817](https://github.com/expo/eas-cli/pull/2817) by [@shiroyasha9](https://github.com/shiroyasha9))
## [14.4.0](https://github.com/expo/eas-cli/releases/tag/v14.4.0) - 2025-01-09
### 🎉 New features
- Load `.env` variables even when `--environment` is specified for `deploy` command. Conflicts will be highlighted by a warning message. ([#2783](https://github.com/expo/eas-cli/pull/2783) by [@kitten](https://github.com/kitten))
- Silence all non-command output in non-interactive mode of eas env:exec. ([#2800](https://github.com/expo/eas-cli/pull/2800) by [@wschurman](https://github.com/wschurman))
- Unhide `deploy` and `deploy:alias` commands ([#2807](https://github.com/expo/eas-cli/pull/2807) by [@kitten](https://github.com/kitten))
## [14.3.1](https://github.com/expo/eas-cli/releases/tag/v14.3.1) - 2025-01-08
### 🧹 Chores
- Bump `@expo/[email protected]` to support Bun text-based lock files. ([#2801](https://github.com/expo/eas-cli/pull/2801) by [@kudo](https://github.com/kudo))
## [14.3.0](https://github.com/expo/eas-cli/releases/tag/v14.3.0) - 2025-01-07
### 🎉 New features
- Upload `package.json` when uploading workflow sources. ([#2786](https://github.com/expo/eas-cli/pull/2786) by [@sjchmiela](https://github.com/sjchmiela))
### 🐛 Bug fixes
- Show `eas deploy` upload error messages. ([#2771](https://github.com/expo/eas-cli/pull/2771) by [@kadikraman](https://github.com/kadikraman))
- Prevent EAS CLI dependencies check from running repeatedly. ([#2781](https://github.com/expo/eas-cli/pull/2781) by [@kitten](https://github.com/kitten))
- Prevent optimistic request body parsing for `eas deploy`. ([#2784](https://github.com/expo/eas-cli/pull/2784) by [@kadikraman](https://github.com/kadikraman))
### 🧹 Chores
- Update log output for `worker` deploy and alias commands. ([#2780](https://github.com/expo/eas-cli/pull/2780) by [@kitten](https://github.com/kitten))
- Update various messages wording. ([#2790](https://github.com/expo/eas-cli/pull/2790) by [@simek](https://github.com/simek))
- Use node18 as tsconfig base. ([#2739](https://github.com/expo/eas-cli/pull/2739) by [@quinlanj](https://github.com/quinlanj))
## [14.2.0](https://github.com/expo/eas-cli/releases/tag/v14.2.0) - 2024-12-13
### 🎉 New features
- Add `eas deploy --dry-run` flag to output tarball. ([#2761](https://github.com/expo/eas-cli/pull/2761) by [@kitten](https://github.com/kitten))
- Allow specifying credentials for android builds. ([#2775](https://github.com/expo/eas-cli/pull/2775) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
- Remove random branch name generation for --auto branch name non-vcs fallback. ([#2747](https://github.com/expo/eas-cli/pull/2747) by [@wschurman](https://github.com/wschurman))
- Upgrade @expo/multipart-body-parser. ([#2751](https://github.com/expo/eas-cli/pull/2751) by [@wschurman](https://github.com/wschurman))
- Pass env var flag to worker deployments. ([#2763](https://github.com/expo/eas-cli/pull/2763) by [@kadikraman](https://github.com/kadikraman))
- Fix request for switching providers when doing Apple auth. ([#2769](https://github.com/expo/eas-cli/pull/2769) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Improve logging and validation in `eas env:exec` command. ([#2762](https://github.com/expo/eas-cli/pull/2762) by [@szdziedzic](https://github.com/szdziedzic))
## [14.1.0](https://github.com/expo/eas-cli/releases/tag/v14.1.0) - 2024-12-10
### 🎉 New features
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "14.1.0",
"version": "16.17.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
@@ -0,0 +1,22 @@
# eas-build-cache-provider
A build cache provider plugin for the Expo CLI
To use the EAS remote build provider plugin, install the `eas-build-cache-provider` package as a developer dependency:
```bash
npm install --save-dev eas-build-cache-provider
```
Then, update your **app.json** to include the `buildCacheProvider` property and its provider under `experiments`:
```json
{
"expo": {
...
"experiments": {
"buildCacheProvider": "eas"
}
}
}
```
@@ -0,0 +1,58 @@
{
"name": "eas-build-cache-provider",
"description": "A build cache provider plugin for the Expo CLI",
"version": "16.4.2",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/config": "11.0.10",
"@expo/spawn-async": "^1.7.2",
"chalk": "4.1.2",
"figures": "3.2.0",
"fs-extra": "11.2.0",
"getenv": "1.0.0",
"log-symbols": "4.1.0",
"semver": "7.5.2",
"terminal-link": "2.1.1",
"tslib": "2.4.1"
},
"devDependencies": {
"@tsconfig/node18": "18.2.4",
"@types/babel__code-frame": "7.0.3",
"@types/fs-extra": "11.0.4",
"@types/getenv": "^1.0.0",
"memfs": "3.4.13",
"rimraf": "3.0.2",
"typescript": "5.3.3"
},
"engines": {
"node": ">=18.0.0"
},
"homepage": "https://github.com/expo/eas-cli",
"license": "MIT",
"main": "build/index.js",
"types": "build/index.d.ts",
"repository": "expo/eas-cli",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
"watch": "yarn build --watch --preserveWatchOutput",
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
"typecheck": "tsc",
"prepack": "yarn rebuild",
"rebuild": "rimraf build && yarn build",
"test": "jest",
"clean": "rimraf build node_modules yarn-error.log"
},
"files": [
"/build"
],
"publishConfig": {
"access": "public"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
}
}
@@ -0,0 +1,19 @@
module.exports = {
extends: ['universe/shared/typescript-analysis'],
overrides: [
{
files: ['*.ts', '*.d.ts'],
parserOptions: {
project: './packages/eas-build-cache-provider/tsconfig.json',
},
rules: {
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
},
},
],
};
@@ -0,0 +1,38 @@
import { RunOptions, getPackageJson } from '@expo/config';
import { SpawnResult } from '@expo/spawn-async';
export function isSpawnResultError(obj: any): obj is Error & SpawnResult {
return (
obj &&
'message' in obj &&
obj.status !== undefined &&
obj.stdout !== undefined &&
obj.stderr !== undefined
);
}
export function isDevClientBuild({
runOptions,
projectRoot,
}: {
runOptions: RunOptions;
projectRoot: string;
}): boolean {
if (!hasDirectDevClientDependency(projectRoot)) {
return false;
}
if ('variant' in runOptions && runOptions.variant !== undefined) {
return runOptions.variant === 'debug';
}
if ('configuration' in runOptions && runOptions.configuration !== undefined) {
return runOptions.configuration === 'Debug';
}
return true;
}
export function hasDirectDevClientDependency(projectRoot: string): boolean {
const { dependencies = {}, devDependencies = {} } = getPackageJson(projectRoot);
return !!dependencies['expo-dev-client'] || !!devDependencies['expo-dev-client'];
}
@@ -0,0 +1,136 @@
import {
BuildCacheProviderPlugin,
CalculateFingerprintHashProps,
ResolveBuildCacheProps,
UploadBuildCacheProps,
} from '@expo/config';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { isDevClientBuild, isSpawnResultError } from './helpers';
import Log from './log';
async function resolveBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
runOptions,
}: ResolveBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip checking for remote builds');
return null;
}
Log.log(
chalk`{whiteBright \u203A} {bold Searching builds with matching fingerprint on EAS servers}`
);
try {
const results = await spawnAsync(
'npx',
[
'eas-cli',
'build:download',
`--platform=${platform}`,
`--fingerprint=${fingerprintHash}`,
'--non-interactive',
isDevClientBuild({ runOptions, projectRoot }) ? '--dev-client' : '--no-dev-client',
'--json',
],
{
cwd: projectRoot,
}
);
Log.log(chalk`{whiteBright \u203A} {bold Successfully downloaded cached build}`);
// {
// "path": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
// }
const json = JSON.parse(results.stdout.trim());
return json?.path;
} catch (error) {
Log.debug('eas-cli error:', error);
// @TODO(2025-04-11): remove this in a future release
if (isSpawnResultError(error) && error.stderr.includes('command build:download not found')) {
Log.warn(
`To take advantage of EAS build cache provider, upgrade your eas-cli installation to latest.`
);
}
return null;
}
}
async function uploadBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
buildPath,
}: UploadBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip uploading builds');
return null;
}
try {
Log.log(chalk`{whiteBright \u203A} {bold Uploading build to EAS}`);
const results = await spawnAsync(
'npx',
[
'eas-cli',
'upload',
`--platform=${platform}`,
`--fingerprint=${fingerprintHash}`,
buildPath ? `--build-path=${buildPath}` : '',
'--non-interactive',
'--json',
],
{
cwd: projectRoot,
}
);
// {
// "url": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
// }
const json = JSON.parse(results.stdout.trim());
Log.log(chalk`{whiteBright \u203A} {bold Build successfully uploaded: ${json?.url}}`);
return json?.url;
} catch (error) {
Log.debug('eas-cli error:', error);
}
return null;
}
async function calculateEASFingerprintHashAsync({
projectRoot,
platform,
}: CalculateFingerprintHashProps): Promise<string | null> {
// prefer using `eas fingerprint:generate` because it automatically upload sources
try {
const results = await spawnAsync(
'npx',
['eas-cli', 'fingerprint:generate', `--platform=${platform}`, '--json', '--non-interactive'],
{
cwd: projectRoot,
}
);
// {
// "hash": "203f960b965e154b77dc31c6c42e5582e8d77196"
// }
const json = JSON.parse(results.stdout.trim());
return json?.hash;
} catch (error) {
Log.debug('eas-cli error:', error);
}
return null;
}
const EASBuildCacheProvider: BuildCacheProviderPlugin = {
resolveBuildCache: resolveBuildCacheAsync,
uploadBuildCache: uploadBuildCacheAsync,
calculateFingerprintHash: calculateEASFingerprintHashAsync,
};
export default EASBuildCacheProvider;
@@ -0,0 +1,121 @@
import chalk from 'chalk';
import figures from 'figures';
import { boolish } from 'getenv';
import logSymbols from 'log-symbols';
import terminalLink from 'terminal-link';
type Color = (...text: string[]) => string;
export default class Log {
public static readonly isDebug = boolish('EXPO_DEBUG', false);
public static log(...args: any[]): void {
Log.consoleLog(...args);
}
public static newLine(): void {
Log.consoleLog();
}
public static addNewLineIfNone(): void {
if (!Log.isLastLineNewLine) {
Log.newLine();
}
}
public static error(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.red));
}
public static warn(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
}
public static debug(...args: any[]): void {
if (Log.isDebug) {
Log.consoleLog(...args);
}
}
public static gray(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
}
public static warnDeprecatedFlag(flag: string, message: string): void {
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
}
public static fail(message: string): void {
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
}
public static succeed(message: string): void {
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
}
public static withTick(...args: any[]): void {
Log.consoleLog(chalk.green(figures.tick), ...args);
}
public static withInfo(...args: any[]): void {
Log.consoleLog(chalk.green(figures.info), ...args);
}
private static consoleLog(...args: any[]): void {
Log.updateIsLastLineNewLine(args);
// eslint-disable-next-line no-console
console.log(...args);
}
private static withTextColor(args: any[], chalkColor: Color): string[] {
return args.map(arg => chalkColor(arg));
}
private static isLastLineNewLine = false;
private static updateIsLastLineNewLine(args: any[]): void {
if (args.length === 0) {
Log.isLastLineNewLine = true;
} else {
const lastArg = args[args.length - 1];
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
Log.isLastLineNewLine = true;
} else {
Log.isLastLineNewLine = false;
}
}
}
}
/**
* Prints a link for given URL, using text if provided, otherwise text is just the URL.
* Format links as dim (unless disabled) and with an underline.
*
* @example https://expo.dev
*/
export function link(
url: string,
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
): string {
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
const output = terminalLink(text, url, {
fallback: () =>
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
});
return dim ? chalk.dim(output) : output;
}
/**
* Provide a consistent "Learn more" link experience.
* Format links as dim (unless disabled) with an underline.
*
* @example Learn more: https://expo.dev
*/
export function learnMore(
url: string,
{
learnMoreMessage: maybeLearnMoreMessage,
dim = true,
}: { learnMoreMessage?: string; dim?: boolean } = {}
): string {
return link(url, { text: maybeLearnMoreMessage ?? 'Learn more', dim });
}
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig",
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}
@@ -0,0 +1,18 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "build",
"rootDir": "src",
"typeRoots": ["../../node_modules/@types", "../../ts-declarations", "./node_modules/@types"]
},
"include": ["src/**/*"]
}
File diff suppressed because it is too large. Load diff
+3
View File
@@ -12,3 +12,6 @@ if (process.env.TMPDIR) {
(fs.realpath as any).native = jest.fn();
module.exports = fs;
// Ensure requiring node:fs returns the mock too. This is needed for expo/config and expo/json-file.
jest.mock('node:fs', () => fs);
+1 -7
View File
@@ -1,13 +1,7 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
- 'src/worker/**/*.ts'
- 'src/**/!(*.d).ts'
generates:
src/graphql/generated.ts:
plugins:
+6281 -593
View File
File diff suppressed because it is too large. Load diff
+12 -7
View File
@@ -1,25 +1,25 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "14.1.0",
"version": "16.17.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "2.1.4",
"@expo/apple-utils": "2.1.12",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.151",
"@expo/eas-json": "14.0.3",
"@expo/env": "^0.4.0",
"@expo/eas-build-job": "1.0.173",
"@expo/eas-json": "16.13.2",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.117",
"@expo/multipart-body-parser": "2.0.0",
"@expo/osascript": "2.1.4",
"@expo/package-manager": "1.6.1",
"@expo/package-manager": "1.7.0",
"@expo/pkcs12": "0.1.3",
"@expo/plist": "0.2.0",
"@expo/plugin-help": "5.1.23",
@@ -28,7 +28,7 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.151",
"@expo/steps": "1.0.173",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -38,6 +38,7 @@
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"bplist-parser": "^0.3.0",
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"dateformat": "4.6.3",
@@ -68,6 +69,7 @@
"nanoid": "3.3.8",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"node-stream-zip": "1.15.0",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
@@ -78,6 +80,7 @@
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.5.4",
"set-interval-async": "3.0.3",
"slash": "3.0.0",
"tar": "6.2.1",
"tar-stream": "3.1.7",
@@ -95,6 +98,7 @@
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@tsconfig/node18": "18.2.4",
"@types/cli-progress": "3.11.5",
"@types/dateformat": "3.0.1",
"@types/diff": "6.0.0",
@@ -230,6 +234,7 @@
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"verify-graphql-code": "./scripts/verify-graphql.sh",
"clean": "rimraf dist build tmp node_modules yarn-error.log"
},
"volta": {
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
function cleanup()
{
echo 'Cleaning up...'
rm -f graphql.schema.json.bak src/graphql/generated.ts.bak
}
# Fail if anything errors
set -eox pipefail
# If this script exits, trap it first and clean up
trap cleanup EXIT
cp graphql.schema.json graphql.schema.json.bak
cp src/graphql/generated.ts src/graphql/generated.ts.bak
yarn generate-graphql-code
if cmp -s graphql.schema.json graphql.schema.json.bak; then
echo "GraphQL schema is up-to-date"
if cmp -s src/graphql/generated.ts src/graphql/generated.ts.bak; then
echo "GraphQL generated code is up-to-date"
exit 0
else
echo "GraphQL code has changed but has not been regenerated. Run `yarn generate-graphql-code` and commit the changes."
exit 1
fi
else
echo "GraphQL schema has changed on the server. Running `yarn tsc` to check for breaking changes..."
yarn tsc
# If there are no errors, then the schema is backwards compatible
exit 0
fi
+3
View File
@@ -6,6 +6,7 @@ export class ApiV2Error extends Error {
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
readonly expoApiV2ErrorRequestId?: string;
constructor(response: {
message: string;
@@ -13,11 +14,13 @@ export class ApiV2Error extends Error {
stack?: string;
details?: JSONValue;
metadata?: object;
requestId?: string;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
this.expoApiV2ErrorRequestId = response.requestId;
}
}
@@ -16,6 +16,7 @@ describe(ApiV2Client, () => {
stack: 'line 1: hello',
details: { who: 'world' },
metadata: { an: 'object' },
requestId: 'request-id',
},
],
});
@@ -37,6 +38,7 @@ describe(ApiV2Client, () => {
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
expect(error.expoApiV2ErrorRequestId).toEqual('request-id');
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
@@ -7,7 +7,16 @@ import { v4 as uuidv4 } from 'uuid';
import { ContextInput, ContextOutput } from '../../commandUtils/EasCommand';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { AppFragment, Role } from '../../graphql/generated';
import {
AppFragment,
Role,
WorkflowJobStatus,
WorkflowJobType,
WorkflowRunByIdWithJobsQuery,
WorkflowRunFragment,
WorkflowRunStatus,
WorkflowRunTriggerEventType,
} from '../../graphql/generated';
export function getMockEasJson(): EasJson {
return {
@@ -81,7 +90,9 @@ export function mockCommandContext<
if (contextKey === 'loggedIn') {
result.loggedIn = {};
}
if (contextKey === 'projectId') {
result.projectId = overrides.projectId ?? mockProjectId;
}
if (contextKey === 'projectDir') {
result.projectDir = projectDir;
}
@@ -168,3 +179,129 @@ export function withLocalVersionSource(easJson: EasJson): EasJson {
},
};
}
export function getMockEmptyWorkflowRunsFragment(): WorkflowRunFragment[] {
return getMockWorkflowRunsFragment();
}
export function getMockWorkflowRunsFragment(
params?:
| undefined
| {
successes?: number;
failures?: number;
pending?: number;
withJobs?: number;
}
): WorkflowRunFragment[] {
const { successes = 0, failures = 0, pending = 0, withJobs = 0 } = params ?? {};
const runs: any[] = [];
for (let i = 0; i < successes; i++) {
runs.push({
id: `success-${i}`,
status: WorkflowRunStatus.Success,
errors: [],
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < failures; i++) {
runs.push({
id: `failure-${i}`,
errors: ['Failed'],
status: WorkflowRunStatus.Failure,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < pending; i++) {
runs.push({
id: `pending-${i}`,
errors: [],
status: WorkflowRunStatus.InProgress,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < withJobs; i++) {
runs.push(getMockWorkflowRunWithJobsFragment());
}
return runs;
}
export function getMockWorkflowJobFragment(
runId?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
return {
id: 'job1',
key: 'job1',
name: 'job1',
status: WorkflowJobStatus.Success,
type: WorkflowJobType.Build,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
outputs: {},
errors: [],
workflowRun: {
id: runId ?? 'build1',
},
turtleJobRun: {
id: 'job1',
logFileUrls: ['https://example.com/log1'],
errors: [],
},
};
}
export function getMockWorkflowRunWithJobsFragment(
runID?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
const id = runID ?? 'build1';
return {
id,
status: WorkflowRunStatus.Failure,
triggerEventType: WorkflowRunTriggerEventType.Manual,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
errors: [],
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
app: {
id: mockProjectId,
__typename: 'App',
name: 'App',
ownerAccount: {
id: 'account-id',
name: 'account-name',
__typename: 'Account',
},
},
},
jobs: [getMockWorkflowJobFragment(id)],
};
}
@@ -0,0 +1,188 @@
import {
getMockEmptyWorkflowRunsFragment,
getMockWorkflowJobFragment,
getMockWorkflowRunWithJobsFragment,
getMockWorkflowRunsFragment,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import { fetchRawLogsForJobAsync } from '../../commandUtils/workflow/fetchLogs';
import WorkflowLogView from '../../commands/workflow/logs';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowJobQuery } from '../../graphql/queries/WorkflowJobQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log from '../../log';
import { promptAsync } from '../../prompts';
jest.mock('../../build/android/version');
jest.mock('../../build/ios/version');
jest.mock('../../project/applicationIdentifier');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../graphql/queries/AppQuery');
jest.mock('../../graphql/queries/WorkflowJobQuery');
jest.mock('../../graphql/queries/WorkflowRunQuery');
jest.mock('../../graphql/mutations/AppVersionMutation');
jest.mock('../../project/workflow');
jest.mock('../../project/android/gradleUtils');
jest.mock('../../project/ios/target');
jest.mock('../../project/ios/scheme');
jest.mock('fs');
jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
jest.mock('../../commandUtils/workflow/fetchLogs');
describe(WorkflowLogView, () => {
beforeEach(() => {
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowJobFragment());
jest
.mocked(WorkflowRunQuery.withJobsByIdAsync)
.mockResolvedValue(getMockWorkflowRunWithJobsFragment());
});
afterEach(() => {
jest.clearAllMocks();
});
test('view logs, passing in no parameters, no runs found', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(Log.error).toHaveBeenCalledWith('No workflow runs found');
});
test('view logs, passing in no parameters, runs found, no jobs', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ successes: 1 }));
jest.mocked(promptAsync).mockResolvedValue({ selectedRun: 'build1' });
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(Log.error).toHaveBeenCalledWith('No job found');
});
test('view logs, passing in no parameters, runs found, jobs found, step selected, no logs', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ withJobs: 1 }));
let promptCalls = 0;
jest.mocked(promptAsync).mockImplementation(async () => {
if (promptCalls === 0) {
promptCalls++;
return { selectedRun: 'build1' };
} else if (promptCalls === 1) {
promptCalls++;
return { selectedJob: 0 };
} else {
return { selectedStep: 'step1' };
}
});
jest.mocked(fetchRawLogsForJobAsync).mockResolvedValue(null);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(promptAsync).toHaveBeenCalledTimes(2);
expect(Log.error).toHaveBeenCalledWith('No logs found');
});
test('view logs, passing in no parameters, runs found, jobs found, step selected, logs found', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ withJobs: 1 }));
let promptCalls = 0;
jest.mocked(promptAsync).mockImplementation(async () => {
if (promptCalls === 0) {
promptCalls++;
return { selectedRun: 'build1' };
} else if (promptCalls === 1) {
promptCalls++;
return { selectedJob: 0 };
} else {
return { selectedStep: 'step1' };
}
});
jest
.mocked(fetchRawLogsForJobAsync)
.mockResolvedValue(
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(Log.error).not.toHaveBeenCalled();
expect(Log.log).toHaveBeenCalledWith(' 2022-01-01T00:00:00.000Z test');
});
test('view logs for a workflow job, passing in a job ID, all steps', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, ['job1', '--all-steps'], ctx);
await cmd.run();
expect(WorkflowJobQuery.byIdAsync).toHaveBeenCalledWith(ctx.loggedIn.graphqlClient, 'job1', {
useCache: false,
});
});
test('view logs for a workflow job, passing in a run ID', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
jest.mocked(promptAsync).mockResolvedValue({ selectedJob: 'job1' });
jest.mocked(WorkflowJobQuery.byIdAsync).mockRejectedValue(new Error('Job not found'));
const cmd = mockTestCommand(WorkflowLogView, ['build1', '--all-steps'], ctx);
await cmd.run();
expect(WorkflowRunQuery.withJobsByIdAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
'build1',
{
useCache: false,
}
);
});
test('view logs for a workflow job, passing in a run ID, non-interactive', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
jest.mocked(promptAsync).mockResolvedValue({ selectedJob: 'job1' });
jest.mocked(WorkflowJobQuery.byIdAsync).mockRejectedValue(new Error('Job not found'));
const cmd = mockTestCommand(WorkflowLogView, ['build1', '--non-interactive'], ctx);
try {
await cmd.run();
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect((e as Error).message).toEqual('No workflow job found that matched the provided ID');
}
});
});
@@ -0,0 +1,184 @@
import {
getMockEmptyWorkflowRunsFragment,
getMockWorkflowRunsFragment,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import WorkflowRunList from '../../commands/workflow/runs';
import { WorkflowRunStatus, WorkflowRunTriggerEventType } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
jest.mock('../../build/android/version');
jest.mock('../../build/ios/version');
jest.mock('../../project/applicationIdentifier');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../graphql/queries/AppQuery');
jest.mock('../../graphql/queries/WorkflowRunQuery');
jest.mock('../../graphql/mutations/AppVersionMutation');
jest.mock('../../project/workflow');
jest.mock('../../project/android/gradleUtils');
jest.mock('../../project/ios/target');
jest.mock('../../project/ios/scheme');
jest.mock('fs');
jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
describe(WorkflowRunList, () => {
afterEach(() => {
jest.clearAllMocks();
});
test('list workflow runs with default params', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
const cmd = mockTestCommand(WorkflowRunList, [], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
10
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list workflow runs with custom limit', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
const cmd = mockTestCommand(WorkflowRunList, ['--limit', '100'], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
100
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list workflow runs with specific filename', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
{
id: 'build',
status: WorkflowRunStatus.Success,
triggerEventType: WorkflowRunTriggerEventType.Manual,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
errors: [],
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
},
},
]);
const cmd = mockTestCommand(WorkflowRunList, ['--workflow', 'build.yml'], ctx);
await cmd.run();
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'build.yml',
undefined,
10
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list only workflow runs with status FAILURE, get json output', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ failures: 1 }));
const cmd = mockTestCommand(WorkflowRunList, ['--status', 'FAILURE', '--json'], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
WorkflowRunStatus.Failure,
10
);
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
{
id: 'failure-0',
status: 'FAILURE',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
trigger: '',
triggerType: 'Other',
startedAt: '2022-01-01T00:00:00.000Z',
finishedAt: '2022-01-01T00:00:00.000Z',
workflowId: 'build',
workflowName: 'build',
workflowFileName: 'build.yml',
},
]);
});
test('list only workflow runs with specific filename and status FAILURE, get json output', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
{
id: 'build1',
status: WorkflowRunStatus.Failure,
triggerEventType: WorkflowRunTriggerEventType.Manual,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
errors: [],
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
},
},
]);
const cmd = mockTestCommand(
WorkflowRunList,
['--workflow', 'build.yml', '--status', 'FAILURE', '--json'],
ctx
);
await cmd.run();
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'build.yml',
WorkflowRunStatus.Failure,
10
);
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
{
id: 'build1',
status: 'FAILURE',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
trigger: '',
triggerType: 'Manual',
startedAt: '2022-01-01T00:00:00.000Z',
finishedAt: '2022-01-01T00:00:00.000Z',
workflowId: 'build',
workflowName: 'build',
workflowFileName: 'build.yml',
},
]);
});
});
+5 -1
View File
@@ -95,7 +95,11 @@ export function getExpoWebsiteBaseUrl(): string {
}
}
export function getEASUpdateURL(projectId: string): string {
export function getEASUpdateURL(projectId: string, manifestHostOverride: string | null): string {
if (manifestHostOverride) {
return new URL(projectId, `https://${manifestHostOverride}`).href;
}
if (process.env.EXPO_STAGING) {
return new URL(projectId, `https://staging-u.expo.dev`).href;
} else if (process.env.EXPO_LOCAL) {
@@ -38,7 +38,7 @@ describe(ensureProjectConfiguredAsync, () => {
}),
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const vcsClientMock = jest.mocked(new GitClient());
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
@@ -60,7 +60,7 @@ describe(ensureProjectConfiguredAsync, () => {
});
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeFalsy();
const vcsClientMock = jest.mocked(new GitClient());
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
@@ -0,0 +1,26 @@
import Log from '../../../log';
import { maybeWarnAboutNonStandardBuildType } from '../build';
const warn = jest.spyOn(Log, 'warn');
describe('maybeWarnAboutNonStandardBuildType', () => {
it('should not warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildRelease',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'release' });
expect(warn).not.toHaveBeenCalled();
});
it('should warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildStage',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'stage' });
expect(warn).toHaveBeenCalled();
});
});
@@ -1,23 +1,43 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile } from '@expo/eas-json/build/build/types';
import assert from 'assert';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import path from 'path';
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import { resolveVcsClient } from '../../../vcs';
import { Client } from '../../../vcs/vcs';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
maybeResolveVersionsAsync,
resolveRemoteVersionCodeAsync,
} from '../version';
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
jest.mock('../../../vcs/vcs');
jest.mock('../../../graphql/queries/AppVersionQuery');
jest.mock('../../../graphql/mutations/AppVersionMutation');
jest.mock('../../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
succeed: jest.fn(),
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
const vcsClient = resolveVcsClient();
@@ -216,12 +236,160 @@ describe(maybeResolveVersionsAsync, () => {
});
});
describe(resolveRemoteVersionCodeAsync, () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('uses current remote versionCode when remote version set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: {} as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('11');
expect(createAppVersionAsyncSpy).not.toHaveBeenCalled();
});
it('initializes versionCode from local files when remote version not set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: 22, versionName: '2.3.4' });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: {} as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('22');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('22');
});
it('initializes versionCode starting with 1 when remote version not set and not set in local files, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: null, versionName: null });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: {} as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
it('increments current remote versionCode when remote version set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('12');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('3.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('12');
});
it('increments versionCode from local files when remote version not set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: 22, versionName: '2.3.4' });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('23');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('23');
});
it('initializes versionCode starting with 1 when remote version not set and not set in local files, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: null, versionName: null });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
});
function initBareWorkflowProject({
versionCode = 123,
versionName = '3.0.0',
}: {
versionCode?: number;
versionName?: string;
versionCode?: number | null;
versionName?: string | null;
} = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
@@ -239,8 +407,8 @@ function initBareWorkflowProject({
'./android/app/build.gradle': `android {
defaultConfig {
applicationId "com.expo.testapp"
versionCode ${versionCode}
versionName "${versionName}"
${versionCode ? `versionCode "${versionCode}"` : ''}
${versionName ? `versionName "${versionName}"` : ''}
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
+42 -1
View File
@@ -1,6 +1,7 @@
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import getenv from 'getenv';
import nullthrows from 'nullthrows';
import { transformJob } from './graphql';
@@ -12,7 +13,7 @@ import AndroidCredentialsProvider, {
} from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import Log, { learnMore } from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
@@ -65,6 +66,10 @@ This means that it will most likely produce an AAB and you will not be able to i
ctx.vcsClient
);
if (gradleContext?.buildType) {
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: gradleContext.buildType });
}
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
}
@@ -151,6 +156,7 @@ async function ensureAndroidCredentialsAsync(
ctx.android.gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
name: ctx.buildProfile.keystoreName,
app: {
account: nullthrows(
ctx.user.accounts.find(a => a.name === ctx.accountName),
@@ -167,3 +173,38 @@ async function ensureAndroidCredentialsAsync(
source: credentialsSource,
};
}
export function maybeWarnAboutNonStandardBuildType({
buildProfile,
buildType,
}: {
buildProfile: Pick<CommonContext<Platform.ANDROID>['buildProfile'], 'gradleCommand'>;
buildType: string;
}): void {
const shouldSuppressWarning = getenv.boolish('EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING', false);
if (shouldSuppressWarning) {
return;
}
const knownValidBuildTypes = ['debug', 'release'];
if (!knownValidBuildTypes.includes(buildType)) {
Log.warn(
`Custom gradle command "${chalk.bold(
buildProfile.gradleCommand
)}" has non-standard build type: "${chalk.bold(
buildType
)}". Expected the command to end with "${chalk.bold('...Debug')}" or "${chalk.bold(
'...Release'
)}"`
);
Log.warn(
`Ensure the command is spelled correctly and ${chalk.bold(
'build.gradle'
)} is configured correctly. To suppress this warning, set ${chalk.bold(
'EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING=true'
)}.`
);
Log.warn(learnMore('https://developer.android.com/build/build-variants#build-types'));
}
}
@@ -8,6 +8,7 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -29,6 +30,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
@@ -12,7 +12,7 @@ import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
import { BuildContext } from '../context';
interface JobData {
@@ -29,7 +29,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Android.Job> {
const username = getUsername(ctx.exp, ctx.user);
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
@@ -57,9 +57,7 @@ export async function prepareJobAsync(
: undefined;
let buildMode;
if (ctx.repack) {
buildMode = BuildMode.REPACK;
} else if (buildProfile.config) {
if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
@@ -73,11 +71,15 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
env: buildProfile.env,
env: {
...buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
},
cache: {
...cacheDefaults,
@@ -92,6 +94,7 @@ export async function prepareJobAsync(
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
buildType,
username,
...(ctx.android.versionCodeOverride && {
@@ -109,11 +112,6 @@ export async function prepareJobAsync(
path: maybeCustomBuildConfigPath,
},
}),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
+16 -4
View File
@@ -99,7 +99,7 @@ export async function maybeResolveVersionsAsync(
exp: ExpoConfig,
buildProfile: BuildProfile<Platform.ANDROID>,
vcsClient: Client
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
): Promise<{ appVersion?: string; appBuildVersion?: string; isVersionInitialized?: boolean }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
if (workflow === Workflow.GENERIC) {
const buildGradle = await getAppBuildGradleAsync(projectDir);
@@ -113,6 +113,11 @@ export async function maybeResolveVersionsAsync(
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
isVersionInitialized: !resolveConfigValue(
buildGradle,
'versionCode',
parsedGradleCommand?.flavor
),
};
} catch {
return {};
@@ -204,14 +209,18 @@ export async function resolveRemoteVersionCodeAsync(
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
let currentBuildVersion: string;
let shouldInitializeVersionCode = false;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localVersions.appBuildVersion) {
Log.warn(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
Log.log(
chalk.green(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
)
);
currentBuildVersion = localVersions.appBuildVersion;
shouldInitializeVersionCode = localVersions.isVersionInitialized ?? false;
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
@@ -221,7 +230,10 @@ export async function resolveRemoteVersionCodeAsync(
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
} else if (
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
shouldInitializeVersionCode
) {
const spinner = ora(
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
).start();
+12 -22
View File
@@ -35,8 +35,10 @@ import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import {
LocalFile,
assertProjectTarballSizeDoesNotExceedLimit,
makeProjectMetadataFileAsync,
makeProjectTarballAsync,
maybeWarnAboutProjectTarballSize,
reviewAndCommitChangesAsync,
} from './utils/repository';
import { BuildEvent } from '../analytics/AnalyticsManager';
@@ -44,6 +46,7 @@ import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import { createFingerprintAsync } from '../fingerprint/cli';
import {
AppPlatform,
BuildFragment,
@@ -66,7 +69,6 @@ import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAs
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createFingerprintAsync } from '../utils/fingerprintCli';
import { printJsonOnlyOutput } from '../utils/json';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
@@ -275,19 +277,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
);
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
`Your project archive is ${formatBytes(
projectTarball.size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
}
maybeWarnAboutProjectTarballSize(projectTarball.size);
assertProjectTarballSizeDoesNotExceedLimit(projectTarball.size);
projectTarballPath = projectTarball.path;
const [bucketKey, { metadataLocation }] = await Promise.all([
@@ -509,11 +500,7 @@ async function handleSingleBuildProgressAsync(
if (build.priority !== BuildPriority.High) {
Log.newLine();
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${link(
formatAccountBillingUrl(accountName)
)}`
);
Log.log(`Sign up for a paid plan at ${link(formatAccountBillingUrl(accountName))}`);
}
Log.newLine();
@@ -709,7 +696,10 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
* It's ok for fingerprintSources or runtimeVersion to be empty
* fingerprintSources only exist if the project is using runtimeVersion.policy: fingerprint
*/
if (!resolvedRuntimeVersion.fingerprint || !resolvedRuntimeVersion.runtimeVersion) {
if (
!resolvedRuntimeVersion.expoUpdatesRuntimeFingerprint ||
!resolvedRuntimeVersion.runtimeVersion
) {
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
};
@@ -717,14 +707,14 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: resolvedRuntimeVersion.runtimeVersion,
fingerprint: resolvedRuntimeVersion.fingerprint,
fingerprint: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprint,
graphqlClient: ctx.graphqlClient,
localBuildMode: ctx.localBuildOptions.localBuildMode,
});
return {
runtimeVersion: uploadedFingerprint.hash,
fingerprintSource: uploadedFingerprint.fingerprintSource,
fingerprintHash: resolvedRuntimeVersion.fingerprintHash ?? undefined,
fingerprintHash: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprintHash ?? undefined,
};
}
+73
View File
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
@@ -52,6 +53,78 @@ async function configureAsync({
}
}
export async function doesBuildProfileExistAsync({
projectDir,
profileName,
}: {
projectDir: string;
profileName: string;
}): Promise<boolean> {
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJson = await easJsonAccessor.readRawJsonAsync();
if (!easJson.build?.[profileName]) {
return false;
}
return true;
} catch (error) {
Log.error(`We were unable to read ${chalk.bold('eas.json')} contents. Error: ${error}.`);
throw error;
}
}
export async function createBuildProfileAsync({
projectDir,
profileName,
profileContents,
vcsClient,
nonInteractive,
}: {
projectDir: string;
profileName: string;
profileContents: Record<string, any>;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<void> {
const spinner = ora(`Adding "${profileName}" build profile to ${chalk.bold('eas.json')}`).start();
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
return {
...easJsonRawObject,
build: {
...easJsonRawObject.build,
[profileName]: profileContents,
},
};
});
await easJsonAccessor.writeAsync();
spinner.succeed(
`Successfully added "${profileName}" build profile to ${chalk.bold('eas.json')}.`
);
if (await vcsClient.isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(
vcsClient,
`Add "${profileName}" build profile to eas.json`,
{
nonInteractive,
}
);
}
} catch (error) {
spinner.fail(
`We were not able to configure "${profileName}" build profile inside of ${chalk.bold(
'eas.json'
)}. Error: ${error}.`
);
throw error;
}
}
const EAS_JSON_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
+2 -1
View File
@@ -62,6 +62,7 @@ export interface BuildContext<T extends Platform> {
requiredPackageManager: NodePackageManager['name'] | null;
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
isVerboseLoggingEnabled: boolean;
whatToTest?: string;
env: Record<string, string>;
}
+6 -3
View File
@@ -42,7 +42,8 @@ export async function createBuildContextAsync<T extends Platform>({
customBuildConfigMetadata,
buildLoggerLevel,
freezeCredentials,
repack,
isVerboseLoggingEnabled,
whatToTest,
env,
}: {
buildProfileName: string;
@@ -64,7 +65,8 @@ export async function createBuildContextAsync<T extends Platform>({
customBuildConfigMetadata?: CustomBuildConfigMetadata;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled: boolean;
whatToTest?: string;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
@@ -150,8 +152,9 @@ export async function createBuildContextAsync<T extends Platform>({
developmentClient,
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
isVerboseLoggingEnabled,
env,
whatToTest,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -1,17 +1,12 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { isEnvironment } from './utils/environment';
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,
@@ -84,13 +79,13 @@ async function resolveEnvVarsAsync({
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS servers: ${Object.keys(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
);
}
@@ -112,7 +107,7 @@ async function resolveEnvVarsAsync({
);
if (overlappingKeys.length > 0) {
Log.warn(
`The following environment variables are defined in both the "${buildProfileName}" build profile "env" configuration and the "${environment.toLowerCase()}" environment on EAS servers: ${overlappingKeys.join(
`The following environment variables are defined in both the "${buildProfileName}" build profile "env" configuration and the "${environment.toLowerCase()}" environment on EAS: ${overlappingKeys.join(
', '
)}. The values from the build profile configuration will be used.`
);
@@ -135,23 +130,18 @@ async function resolveEnvVarsAsync({
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
buildProfile: BuildProfile
): EnvironmentVariableEnvironment {
const setEnvironmentMessage =
'Set the "environment" field in the build profile if you want to specify the environment manually.';
if (buildProfile.distribution === 'store') {
Log.log(
`We detected that you are building for the "store" distribution. Resolving the environment for environment variables used during the build to "production". ${setEnvironmentMessage}`
);
return EnvironmentVariableEnvironment.Production;
} else {
if (buildProfile.developmentClient) {
Log.log(
`We detected that you are building the development client. Resolving the environment for environment variables used during the build to "development". ${setEnvironmentMessage}`
);
return EnvironmentVariableEnvironment.Development;
}
Log.log(
`We detected that you are building for the "internal" distribution. Resolving the environment for environment variables used during the build to "preview". ${setEnvironmentMessage}`
);
return EnvironmentVariableEnvironment.Preview;
}
const environment =
buildProfile.distribution === 'store'
? EnvironmentVariableEnvironment.Production
: buildProfile.developmentClient
? EnvironmentVariableEnvironment.Development
: EnvironmentVariableEnvironment.Preview;
Log.log(
`Resolved "${environment.toLowerCase()}" environment for the build. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
)}`
);
return environment;
}
@@ -1,12 +1,19 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { IosBuildProfile } from '@expo/eas-json/build/build/types';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { instance, mock } from 'ts-mockito';
import type { XCBuildConfiguration } from 'xcode';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { Target } from '../../../credentials/ios/types';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { readPlistAsync } from '../../../utils/plist';
import { resolveVcsClient } from '../../../vcs';
import { Client } from '../../../vcs/vcs';
import {
BumpStrategy,
bumpVersionAsync,
@@ -15,9 +22,23 @@ import {
getInfoPlistPath,
readBuildNumberAsync,
readShortVersionAsync,
resolveRemoteBuildNumberAsync,
} from '../version';
jest.mock('fs');
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
jest.mock('../../../vcs/vcs');
jest.mock('../../../graphql/queries/AppVersionQuery');
jest.mock('../../../graphql/mutations/AppVersionMutation');
jest.mock('../../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
succeed: jest.fn(),
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
const vcsClient = resolveVcsClient();
@@ -359,6 +380,154 @@ describe(getInfoPlistPath, () => {
});
});
describe(resolveRemoteBuildNumberAsync, () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('uses current remote buildNumber when remote version set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: {} as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('11');
expect(createAppVersionAsyncSpy).not.toHaveBeenCalled();
});
it('initializes buildNumber from local files when remote version not set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '22', appVersion: '2.3.4' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: {} as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('22');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('22');
});
it('initializes buildNumber starting with 1 when remote version not set and local version is 1, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '1', appVersion: '1.0.0' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: {} as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
it('increments current remote buildNumber when remote version set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: { autoIncrement: true } as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('12');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('12');
});
it('increments buildNumber from local files when remote version not set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '22', appVersion: '2.3.4' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: { autoIncrement: true } as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('23');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('23');
});
it('initializes buildNumber starting with 1 when remote version not set and local version is 1, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '1', appVersion: '1.0.0' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: { autoIncrement: true } as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
});
function initBareWorkflowProject({
appVersion = '1.0.0',
buildNumber = '1',
+2
View File
@@ -10,6 +10,7 @@ import { IosCredentials } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
@@ -28,6 +29,7 @@ export async function createIosContextAsync(
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync(ctx);
}
checkNodeEnvVariable(ctx);
@@ -9,6 +9,7 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -31,6 +32,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
+9 -11
View File
@@ -14,7 +14,7 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
import { BuildContext } from '../context';
interface JobData {
@@ -32,7 +32,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Ios.Job> {
const username = getUsername(ctx.exp, ctx.user);
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
@@ -49,9 +49,7 @@ export async function prepareJobAsync(
: undefined;
let buildMode;
if (ctx.repack) {
buildMode = BuildMode.REPACK;
} else if (buildProfile.config) {
if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
@@ -65,13 +63,17 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
env: buildProfile.env,
env: {
...buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
},
cache: {
...cacheDefaults,
@@ -88,6 +90,7 @@ export async function prepareJobAsync(
buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
username,
...(ctx.ios.buildNumberOverride && {
version: {
@@ -104,11 +107,6 @@ export async function prepareJobAsync(
path: maybeCustomBuildConfigPath,
},
}),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
+10 -3
View File
@@ -330,14 +330,18 @@ export async function resolveRemoteBuildNumberAsync(
vcsClient
);
let currentBuildVersion: string;
let shouldInitializeBuildNumber = false;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localBuildNumber) {
Log.warn(
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
Log.log(
chalk.green(
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
)
);
currentBuildVersion = localBuildNumber;
shouldInitializeBuildNumber = localBuildNumber === '1';
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
@@ -347,7 +351,10 @@ export async function resolveRemoteBuildNumberAsync(
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
} else if (
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
shouldInitializeBuildNumber
) {
const spinner = ora(
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
).start();
+1 -1
View File
@@ -7,7 +7,7 @@ import Log from '../log';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.152';
const PLUGIN_PACKAGE_VERSION = '1.0.171';
export enum LocalBuildMode {
/**
+5 -3
View File
@@ -9,7 +9,10 @@ import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios
import { LocalBuildMode } from './local';
import { BuildDistributionType } from './types';
import Log from '../log';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import {
getUsernameForBuildMetadataAndBuildJob,
isExpoUpdatesInstalled,
} from '../project/projectUtils';
import { readChannelSafelyAsync as readAndroidChannelSafelyAsync } from '../update/android/UpdatesModule';
import { readChannelSafelyAsync as readIosChannelSafelyAsync } from '../update/ios/UpdatesModule';
import { easCliVersion } from '../utils/easCli';
@@ -48,7 +51,7 @@ export async function collectMetadataAsync<T extends Platform>(
ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL
? false
: await ctx.vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, ctx.user),
username: getUsernameForBuildMetadataAndBuildJob(ctx.user),
message: ctx.message,
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
@@ -62,7 +65,6 @@ export async function collectMetadataAsync<T extends Platform>(
requiredPackageManager: ctx.requiredPackageManager ?? undefined,
selectedImage: ctx.buildProfile.image,
customNodeVersion: ctx.buildProfile.node,
environment: ctx.buildProfile.environment,
simulator: 'simulator' in ctx.buildProfile && ctx.buildProfile.simulator,
};
return sanitizeMetadata(metadata);
+86 -31
View File
@@ -11,6 +11,7 @@ import {
import { LoggerLevel } from '@expo/logger';
import assert from 'assert';
import chalk from 'chalk';
import { pathExists } from 'fs-extra';
import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from './android/build';
@@ -62,7 +63,7 @@ import {
validateBuildProfileVersionSettingsAsync,
} from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts';
import { runAsync } from '../run/run';
import { getEasBuildRunCachedAppPath, runAsync } from '../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
import { createSubmissionContextAsync } from '../submit/context';
import {
@@ -96,19 +97,33 @@ export interface BuildFlags {
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled?: boolean;
whatToTest?: string;
}
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
vcsClient: Client,
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
): Promise<{
export async function runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flags,
actor,
getDynamicPrivateProjectConfigAsync,
downloadSimBuildAutoConfirm,
envOverride,
}: {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
projectDir: string;
flags: BuildFlags;
actor: Actor;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
downloadSimBuildAutoConfirm?: boolean;
envOverride?: Env;
}): Promise<{
buildIds: string[];
buildProfiles?: ProfileData<'build'>[];
}> {
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
@@ -192,13 +207,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 { env } = !envOverride
? await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
})
: { env: envOverride };
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
@@ -213,6 +230,7 @@ export async function runBuildAndSubmitAsync(
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
env,
easJsonAccessor,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -255,7 +273,7 @@ export async function runBuildAndSubmitAsync(
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
)
).profile;
const submission = await prepareAndStartSubmissionAsync({
const submission = await prepareAndStartAutoSubmissionAsync({
build: startedBuild.build,
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
@@ -308,7 +326,7 @@ export async function runBuildAndSubmitAsync(
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags, downloadSimBuildAutoConfirm);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
@@ -333,6 +351,7 @@ export async function runBuildAndSubmitAsync(
}
return {
buildIds: startedBuilds.map(({ build }) => build.id),
buildProfiles,
};
}
@@ -349,6 +368,7 @@ async function prepareAndStartBuildAsync({
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
env,
easJsonAccessor,
}: {
projectDir: string;
flags: BuildFlags;
@@ -361,6 +381,7 @@ async function prepareAndStartBuildAsync({
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
easJsonAccessor: EasJsonAccessor;
env: Env;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
@@ -383,7 +404,8 @@ async function prepareAndStartBuildAsync({
customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
whatToTest: flags.whatToTest,
env,
});
@@ -407,8 +429,18 @@ async function prepareAndStartBuildAsync({
nonInteractive: flags.nonInteractive,
buildProfile,
env: buildProfile.profile.env,
easJsonAccessor,
});
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
if (
isUsingEASUpdate(
buildCtx.exp,
buildCtx.projectId,
easJsonCliConfig.updateManifestHostOverride ?? null
)
) {
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
appId: buildCtx.projectId,
channelName: buildProfile.profile.channel,
@@ -473,7 +505,7 @@ async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragme
return await sendBuildRequestAsync();
}
async function prepareAndStartSubmissionAsync({
async function prepareAndStartAutoSubmissionAsync({
build,
buildCtx,
moreBuilds,
@@ -508,6 +540,8 @@ async function prepareAndStartSubmissionAsync({
vcsClient: buildCtx.vcsClient,
isVerboseFastlaneEnabled: false,
specifiedProfile: selectedSubmitProfileName,
groups: undefined, // use groups from submit profile
whatToTest: buildCtx.whatToTest,
});
if (moreBuilds) {
@@ -531,18 +565,29 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
}
}
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
export async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
assert(build.artifacts?.applicationArchiveUrl);
const cachedAppPath = getEasBuildRunCachedAppPath(build.project.id, build.id, build.platform);
if (await pathExists(cachedAppPath)) {
Log.newLine();
Log.log(`Using cached app...`);
await runAsync(cachedAppPath, build.platform);
return;
}
const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
build.platform
build.platform,
cachedAppPath
);
await runAsync(buildPath, build.platform);
}
async function maybeDownloadAndRunSimulatorBuildsAsync(
builds: MaybeBuildFragment[],
flags: BuildFlags
flags: BuildFlags,
autoConfirm?: boolean
): Promise<void> {
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
@@ -550,11 +595,15 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
for (const simBuild of simBuilds) {
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
Log.newLine();
const confirm = await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
});
const confirm =
autoConfirm ??
(await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${
simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'
}?`,
}));
if (confirm) {
await downloadAndRunAsync(simBuild);
}
@@ -572,6 +621,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
nonInteractive,
sdkVersion,
env,
easJsonAccessor,
}: {
exp: ExpoConfig;
projectId: string;
@@ -581,6 +631,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
nonInteractive: boolean;
sdkVersion?: string;
env: Env | undefined;
easJsonAccessor: EasJsonAccessor;
}): Promise<void> {
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
return;
@@ -602,6 +653,9 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
message: `Would you like to install the "expo-updates" package and configure EAS Update now?`,
});
if (installExpoUpdates) {
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
await ensureEASUpdateIsConfiguredAsync({
exp,
projectId,
@@ -609,6 +663,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
platform: RequestedPlatform.All,
vcsClient,
env,
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
});
Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.');
-1
View File
@@ -35,5 +35,4 @@ export interface BuildFlags {
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
}
@@ -0,0 +1,26 @@
import { BuildProfile } from '@expo/eas-json';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
type Environment = NonNullable<BuildProfile['environment']>;
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
production: EnvironmentVariableEnvironment.Production,
preview: EnvironmentVariableEnvironment.Preview,
development: EnvironmentVariableEnvironment.Development,
};
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export function buildProfileEnvironmentToEnvironment(
environment: BuildProfile['environment']
): EnvironmentVariableEnvironment | null {
if (!environment) {
return null;
}
return BuildProfileEnvironmentToEnvironment[environment];
}
@@ -43,14 +43,14 @@ const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) =>
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
Log.log(`See logs: ${link(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${appPlatformEmojis[build.platform]} ${
`See ${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build details: ${link(logsUrl)}`
} logs: ${link(logsUrl)}`
);
});
}
+29 -1
View File
@@ -4,7 +4,7 @@ import path from 'path';
import tar from 'tar';
import { v4 as uuidv4 } from 'uuid';
import Log from '../../log';
import Log, { learnMore } from '../../log';
import { ora } from '../../ora';
import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
@@ -169,6 +169,34 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
return { size, path: tarPath };
}
export function maybeWarnAboutProjectTarballSize(size: number): void {
if (size <= 150 /* MiB */ * 1024 /* KiB */ * 1024 /* B */) {
return;
}
Log.warn(
`Your project archive is ${formatBytes(
size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
const MAX_ALLOWED_PROJECT_TARBALL_SIZE =
2 /* GiB */ * 1024 /* MiB */ * 1024 /* KiB */ * 1024; /* B */
export function assertProjectTarballSizeDoesNotExceedLimit(size: number): void {
if (size <= MAX_ALLOWED_PROJECT_TARBALL_SIZE) {
return;
}
throw new Error(
`Project archive is too big. Maximum allowed size is ${formatBytes(
MAX_ALLOWED_PROJECT_TARBALL_SIZE
)}.`
);
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
+9
View File
@@ -72,3 +72,12 @@ export function getProjectGitHubSettingsUrl(accountName: string, projectName: st
getExpoWebsiteBaseUrl()
).toString();
}
export function getHostingDeploymentsUrl(accountName: string, projectName: string): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/hosting/deployments`,
getExpoWebsiteBaseUrl()
).toString();
}
@@ -18,6 +18,7 @@ export const testUpdateBranch1: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/bbe3e63a-f620-4603-8c4d-258f3e51590d',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
@@ -43,6 +44,7 @@ export const testUpdateBranch1: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/db22401b-d321-4629-a068-21d6d1f19c02',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
@@ -78,6 +80,7 @@ export const testUpdateBranch2: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/f04aafc1-0bf5-4548-8c35-726b467e7c40',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
@@ -103,6 +106,7 @@ export const testUpdateBranch2: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/d8d15786-2727-407f-bf3e-f4bda7e397d9',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
+1 -1
View File
@@ -289,7 +289,7 @@ export async function doesChannelExistAsync(
{ appId, channelName }: { appId: string; channelName: string }
): Promise<boolean> {
try {
await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
appId,
channelName,
});
@@ -27,7 +27,7 @@ import {
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import Log from '../log';
import Log, { link } from '../log';
import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
@@ -264,6 +264,16 @@ export default abstract class EasCommand extends Command {
const defaultMsg = `${messageLine}${requestIdLine}`;
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
if (defaultMsg.includes('ScopedAccountActorViewerContext') && process.env.EAS_BUILD) {
// We're in EAS, authenticated with a scoped account actor access token.
// We may have not added the scoped actor privacy rule to the right place yet.
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nWe are in the process of migrating EAS to a more granular permissioning system. If you believe what you're doing is a legitimate operation you should be able to perform, report this to us at ${link(
'https://expo.dev/contact'
)}\n\nOriginal error message: ${defaultMsg}`;
}
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nThis can sometimes happen if you are logged in as incorrect user.\nRun ${chalk.bold(
@@ -43,6 +43,7 @@ export async function fetchBuildsAsync({
platform?: RequestedPlatform;
profile?: string;
hasFingerprint?: boolean;
fingerprintHash?: string;
};
}): Promise<BuildFragment[]> {
let builds: BuildFragment[];
@@ -56,6 +57,9 @@ export async function fetchBuildsAsync({
if (filters?.hasFingerprint) {
queryFilters['hasFingerprint'] = filters.hasFingerprint;
}
if (filters?.fingerprintHash) {
queryFilters['fingerprintHash'] = filters.fingerprintHash;
}
if (!filters?.statuses) {
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
@@ -5,7 +5,7 @@ import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPublicExpoConfigAsync } from '../../project/expoConfig';
type GetServerSideEnvironmentVariablesFn = (
export type GetServerSideEnvironmentVariablesFn = (
maybeEnv?: Record<string, string>
) => Promise<Record<string, string>>;
@@ -1,6 +1,10 @@
import { vol } from 'memfs';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { easCliVersion } from '../../../../utils/easCli';
import {
findProjectDirAndVerifyProjectSetupAsync,
findProjectRootAsync,
} from '../findProjectDirAndVerifyProjectSetupAsync';
jest.mock('@expo/config');
jest.mock('fs');
@@ -60,3 +64,54 @@ describe(findProjectRootAsync, () => {
expect(projectRoot).toBe('/app');
});
});
describe(findProjectDirAndVerifyProjectSetupAsync, () => {
it('throws if the CLI version is not satisfied', async () => {
vol.fromJSON(
{
'./eas.json': JSON.stringify({ cli: { version: '1.0.0' } }),
'./package.json': JSON.stringify({}),
},
'/app'
);
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).rejects.toThrow(
new RegExp(
`^You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json \\(1.0.0\\)`,
's'
)
);
process.env.EAS_SKIP_CLI_VERSION_CHECK = 'true';
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).resolves.not.toThrow();
delete process.env.EAS_SKIP_CLI_VERSION_CHECK;
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).rejects.toThrow(
new RegExp(
`^You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json \\(1.0.0\\)`,
's'
)
);
});
it('does not throw if the CLI version is satisfied', async () => {
vol.fromJSON(
{
'./eas.json': JSON.stringify({ cli: { version: '>= 1.0.0' } }),
'./package.json': JSON.stringify({}),
},
'/app'
);
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).resolves.not.toThrow();
});
it('does not throw if there is no version', async () => {
vol.fromJSON(
{
'./eas.json': JSON.stringify({}),
'./package.json': JSON.stringify({}),
},
'/app'
);
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).resolves.not.toThrow();
});
});
@@ -78,7 +78,12 @@ describe(getProjectIdAsync, () => {
it('gets the project ID from app config if exists', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -91,7 +96,7 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ sdkVersion: '52.0.0', name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).resolves.toEqual('1234');
@@ -99,7 +104,13 @@ describe(getProjectIdAsync, () => {
it('throws when the owner is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
exp: {
sdkVersion: '54.0.0',
name: 'test',
slug: 'test',
owner: 'wat',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -112,7 +123,13 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
{
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
owner: 'wat',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false }
)
).rejects.toThrow(
@@ -122,83 +139,143 @@ describe(getProjectIdAsync, () => {
);
});
it('throws when the owner is not specified and is different than logged in user', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
describe('when sdkVersion is less than 53', () => {
it('throws when the owner is not specified and is different than logged in user', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'Robot',
id: 'robot_id',
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
const sessionManagerRobot = instance(sessionManagerMock);
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManagerRobot,
{
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
});
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'Robot',
id: 'robot_id',
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
const sessionManagerRobot = instance(sessionManagerMock);
describe('when sdkVersion is 53 or higher', () => {
it('does not throw when the owner is not specified', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '53.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
await expect(
getProjectIdAsync(
sessionManager,
{
sdkVersion: '53.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false }
)
).resolves.not.toThrow();
});
await expect(
getProjectIdAsync(
sessionManagerRobot,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the slug is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'wat',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -211,7 +288,7 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
@@ -222,10 +299,17 @@ describe(getProjectIdAsync, () => {
});
it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest
.mocked(getConfig)
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } },
config: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '2345' } },
},
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
@@ -233,7 +317,7 @@ describe(getProjectIdAsync, () => {
const projectId = await getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test' },
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
{
nonInteractive: false,
}
@@ -252,7 +336,9 @@ describe(getProjectIdAsync, () => {
});
it('throws if writing the ID back to the config fails', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest
.mocked(getConfig)
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'fail',
config: null,
@@ -262,13 +348,22 @@ describe(getProjectIdAsync, () => {
.mockImplementation(async () => '4567');
await expect(
getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
getProjectIdAsync(
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
{ nonInteractive: false }
)
).rejects.toThrow();
});
it('throws if extra.eas.projectId is not a string', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: 1234 } } },
exp: {
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: 1234 } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
@@ -281,7 +376,7 @@ describe(getProjectIdAsync, () => {
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
{ nonInteractive: false }
)
).rejects.toThrow(
@@ -2,6 +2,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import * as PackageManagerUtils from '@expo/package-manager';
import chalk from 'chalk';
import fs from 'fs-extra';
import getenv from 'getenv';
import path from 'path';
import pkgDir from 'pkg-dir';
import semver from 'semver';
@@ -13,7 +14,14 @@ import { resolveVcsClient } from '../../../vcs';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
const shouldSkipVersionCheck = getenv.boolish('EAS_SKIP_CLI_VERSION_CHECK', false);
if (
!shouldSkipVersionCheck &&
config?.version &&
!semver.satisfies(easCliVersion, config.version)
) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json (${
config.version
@@ -103,6 +111,8 @@ export async function findProjectRootAsync({
}
}
let ranEnsureEasCliIsNotInDependencies = false;
/**
* Determine the project root directory and ensure some constraints about the project setup
* like CLI version and dependencies.
@@ -110,9 +120,16 @@ export async function findProjectRootAsync({
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
const projectDir = await findProjectRootAsync();
export async function findProjectDirAndVerifyProjectSetupAsync({
cwd,
}: {
cwd?: string;
} = {}): Promise<string> {
const projectDir = await findProjectRootAsync({ cwd });
await applyCliConfigAsync(projectDir);
await ensureEasCliIsNotInDependenciesAsync(projectDir);
if (!ranEnsureEasCliIsNotInDependencies) {
ranEnsureEasCliIsNotInDependencies = true;
await ensureEasCliIsNotInDependenciesAsync(projectDir);
}
return projectDir;
}
@@ -1,6 +1,7 @@
import { ExpoConfig, getProjectConfigDescription } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import semver from 'semver';
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
@@ -127,29 +128,33 @@ export async function validateOrSetProjectIdAsync({
);
}
const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
if (actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
} else {
// robot caller
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
const sdkVersion = exp.sdkVersion;
// SDK 53 and above no longer require owner field in app config
if (sdkVersion && semver.satisfies(sdkVersion, '< 53.0.0')) {
const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
if (actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
} else {
// robot caller
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
}
}
@@ -175,7 +180,7 @@ export async function validateOrSetProjectIdAsync({
Log.warn('EAS project not configured.');
const getAccountNameForEASProjectSync = (exp: ExpoConfig, user: Actor): string => {
const getDefaultAccountNameForEASProject = (exp: ExpoConfig, user: Actor): string => {
if (exp.owner) {
return exp.owner;
}
@@ -186,7 +191,7 @@ export async function validateOrSetProjectIdAsync({
return user.username;
case 'Robot':
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
'Must configure EAS project by running "eas init" before using a robot user to manage the project.'
);
}
};
@@ -194,7 +199,7 @@ export async function validateOrSetProjectIdAsync({
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
graphqlClient,
{
accountName: getAccountNameForEASProjectSync(exp, actor),
accountName: getDefaultAccountNameForEASProject(exp, actor),
projectName: exp.slug,
},
{
@@ -42,13 +42,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS servers: ${Object.keys(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
);
}
Log.newLine();
@@ -0,0 +1,15 @@
import { WorkflowJobResult } from './types';
// This function is in a separate module for testing purposes
export async function fetchRawLogsForJobAsync(job: WorkflowJobResult): Promise<string | null> {
const firstLogFileUrl = job.turtleJobRun?.logFileUrls?.[0];
if (!firstLogFileUrl) {
return null;
}
const response = await fetch(firstLogFileUrl, {
method: 'GET',
});
const rawLogs = await response.text();
return rawLogs;
}
@@ -0,0 +1,324 @@
import { Choice } from 'prompts';
import { WorkflowJobResult, WorkflowLogs } from './types';
import {
choiceFromWorkflowJob,
choiceFromWorkflowRun,
choicesFromWorkflowLogs,
processLogsFromJobAsync,
processWorkflowRuns,
} from './utils';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowJobQuery } from '../../graphql/queries/WorkflowJobQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
/*
* State machine types and functions for moving between different workflow command states
*/
export enum WorkflowCommandSelectionStateValue {
WORKFLOW_RUN_SELECTION = 'WORKFLOW_RUN_SELECTION',
WORKFLOW_JOB_SELECTION = 'WORKFLOW_JOB_SELECTION',
WORKFLOW_STEP_SELECTION = 'WORKFLOW_STEP_SELECTION',
FINISH = 'FINISH',
ERROR = 'ERROR',
}
const workflowCommandSelectionAllowedStateTransitions: {
[key in WorkflowCommandSelectionStateValue]: WorkflowCommandSelectionStateValue[];
} = {
WORKFLOW_JOB_SELECTION: [
WorkflowCommandSelectionStateValue.WORKFLOW_STEP_SELECTION,
WorkflowCommandSelectionStateValue.WORKFLOW_RUN_SELECTION,
WorkflowCommandSelectionStateValue.ERROR,
WorkflowCommandSelectionStateValue.FINISH,
],
WORKFLOW_RUN_SELECTION: [
WorkflowCommandSelectionStateValue.WORKFLOW_JOB_SELECTION,
WorkflowCommandSelectionStateValue.ERROR,
],
WORKFLOW_STEP_SELECTION: [
WorkflowCommandSelectionStateValue.WORKFLOW_JOB_SELECTION,
WorkflowCommandSelectionStateValue.ERROR,
WorkflowCommandSelectionStateValue.FINISH,
],
ERROR: [WorkflowCommandSelectionStateValue.WORKFLOW_RUN_SELECTION],
FINISH: [],
};
export type WorkflowCommandSelectionState = {
graphqlClient: ExpoGraphqlClient;
projectId: string;
nonInteractive: boolean;
allSteps: boolean;
state: WorkflowCommandSelectionStateValue;
runId?: string;
jobId?: string;
step?: string;
job?: WorkflowJobResult;
logs?: WorkflowLogs | null;
message?: string;
};
export type WorkflowCommandSelectionStateParameters = Omit<
WorkflowCommandSelectionState,
'graphqlClient' | 'projectId' | 'state' | 'nonInteractive' | 'allSteps'
>;
export type WorkflowCommandSelectionAction = (
prevState: WorkflowCommandSelectionState
) => Promise<WorkflowCommandSelectionState>;
export function moveToNewWorkflowCommandSelectionState(
previousState: WorkflowCommandSelectionState,
newStateValue: WorkflowCommandSelectionStateValue,
parameters: WorkflowCommandSelectionStateParameters
): WorkflowCommandSelectionState {
if (
!workflowCommandSelectionAllowedStateTransitions[previousState.state].includes(newStateValue)
) {
const errorMessage = `Invalid state transition from ${
previousState.state
} to ${newStateValue}. Allowed transitions from ${
previousState.state
}: ${workflowCommandSelectionAllowedStateTransitions[previousState.state].join(', ')}`;
throw new Error(errorMessage);
}
return {
...previousState,
state: newStateValue,
...parameters,
};
}
export function moveToWorkflowRunSelectionState(
previousState: WorkflowCommandSelectionState,
params?: {
runId?: string | undefined;
}
): WorkflowCommandSelectionState {
return moveToNewWorkflowCommandSelectionState(
previousState,
WorkflowCommandSelectionStateValue.WORKFLOW_RUN_SELECTION,
{
runId: params?.runId,
jobId: undefined,
}
);
}
export function moveToWorkflowJobSelectionState(
previousState: WorkflowCommandSelectionState,
params: {
jobId?: string | undefined;
runId?: string | undefined;
}
): WorkflowCommandSelectionState {
return moveToNewWorkflowCommandSelectionState(
previousState,
WorkflowCommandSelectionStateValue.WORKFLOW_JOB_SELECTION,
params
);
}
export function moveToWorkflowStepSelectionState(
previousState: WorkflowCommandSelectionState,
params: {
job: WorkflowJobResult;
}
): WorkflowCommandSelectionState {
return moveToNewWorkflowCommandSelectionState(
previousState,
WorkflowCommandSelectionStateValue.WORKFLOW_STEP_SELECTION,
params
);
}
export function moveToWorkflowSelectionFinishedState(
previousState: WorkflowCommandSelectionState,
params: {
step: string;
logs: WorkflowLogs;
}
): WorkflowCommandSelectionState {
return moveToNewWorkflowCommandSelectionState(
previousState,
WorkflowCommandSelectionStateValue.FINISH,
params
);
}
export function moveToWorkflowSelectionErrorState(
previousState: WorkflowCommandSelectionState,
message: string
): WorkflowCommandSelectionState {
return moveToNewWorkflowCommandSelectionState(
previousState,
WorkflowCommandSelectionStateValue.ERROR,
{
message,
}
);
}
// eslint-disable-next-line async-protect/async-suffix
export const workflowRunSelectionAction: WorkflowCommandSelectionAction = async prevState => {
const { graphqlClient, projectId, runId, jobId, allSteps } = prevState;
Log.debug(
`workflowRunSelectionAction: runId = ${runId}, jobId = ${jobId}, allSteps = ${allSteps}`
);
if (runId) {
return moveToWorkflowJobSelectionState(prevState, { runId });
}
const runs = await AppQuery.byIdWorkflowRunsFilteredByStatusAsync(
graphqlClient,
projectId,
undefined,
20
);
if (runs.length === 0) {
return moveToWorkflowSelectionErrorState(prevState, 'No workflow runs found');
}
const processedRuns = processWorkflowRuns(runs);
const choices = processedRuns.map(run => choiceFromWorkflowRun(run));
const selectedId = (
await promptAsync({
type: 'select',
name: 'selectedRun',
message: 'Select a workflow run:',
choices,
})
).selectedRun;
return moveToWorkflowJobSelectionState(prevState, { runId: selectedId });
};
// eslint-disable-next-line async-protect/async-suffix
export const workflowJobSelectionAction: WorkflowCommandSelectionAction = async prevState => {
const { graphqlClient, runId, jobId, nonInteractive, allSteps } = prevState;
Log.debug(
`workflowJobSelectionAction: runId = ${runId}, jobId = ${jobId}, allSteps = ${allSteps}`
);
if (jobId) {
let workflowJobResult = undefined;
try {
workflowJobResult = await WorkflowJobQuery.byIdAsync(graphqlClient, jobId, {
useCache: false,
});
return moveToWorkflowStepSelectionState(prevState, { job: workflowJobResult });
} catch {}
if (nonInteractive && !workflowJobResult) {
return moveToWorkflowSelectionErrorState(
prevState,
'No workflow job found that matched the provided ID'
);
} else {
// The passed in ID may be a run ID, pass it back to run selection action
return moveToWorkflowRunSelectionState(prevState, { runId: jobId });
}
} else {
// No job ID was passed in
if (nonInteractive) {
return moveToWorkflowSelectionErrorState(
prevState,
'No workflow job ID provided in non-interactive mode'
);
} else if (!runId) {
// If no jobId or runId, we should go back to run selection
return moveToWorkflowRunSelectionState(prevState);
}
const workflowRunResult = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, runId, {
useCache: false,
});
if (!workflowRunResult) {
return moveToWorkflowSelectionErrorState(
prevState,
'No workflow run found that matched the provided ID'
);
}
const choices: Choice[] = [
...workflowRunResult.jobs.map((job, i) => choiceFromWorkflowJob(job, i)),
{
title: 'Go back and select a different workflow run',
value: -1,
},
];
const selectedJobIndex = (
await promptAsync({
type: 'select',
name: 'selectedJob',
message: 'Select a job:',
choices,
})
).selectedJob;
if (selectedJobIndex === -1) {
return moveToWorkflowRunSelectionState(prevState);
}
const selectedJob = workflowRunResult.jobs[selectedJobIndex] as WorkflowJobResult;
return moveToWorkflowStepSelectionState(prevState, { job: selectedJob });
}
};
// eslint-disable-next-line async-protect/async-suffix
export const workflowStepSelectionAction: WorkflowCommandSelectionAction = async prevState => {
const { job, allSteps } = prevState;
Log.debug(`workflowStepSelectionAction: job = ${job?.id}, allSteps = ${allSteps}`);
if (!job) {
return moveToWorkflowSelectionErrorState(prevState, 'No job found');
}
const logs = await processLogsFromJobAsync(job);
if (!logs) {
return moveToWorkflowSelectionErrorState(prevState, 'No logs found');
}
if (allSteps) {
return moveToWorkflowSelectionFinishedState(prevState, { step: '', logs });
}
const choices: Choice[] = [
...choicesFromWorkflowLogs(logs),
{
title: 'Go back and select a different workflow job',
value: 'go-back',
},
];
const selectedStep: string =
(
await promptAsync({
type: 'select',
name: 'selectedStep',
message: 'Select a step:',
choices,
})
).selectedStep ?? '';
if (selectedStep === 'go-back') {
return moveToWorkflowJobSelectionState(prevState, {
runId: job.workflowRun.id,
jobId: undefined,
});
}
return moveToWorkflowSelectionFinishedState(prevState, { step: selectedStep, logs });
};
export const executeWorkflowSelectionActionsAsync: WorkflowCommandSelectionAction =
async prevState => {
let currentState = prevState;
while (
currentState.state !== WorkflowCommandSelectionStateValue.FINISH &&
currentState.state !== WorkflowCommandSelectionStateValue.ERROR
) {
Log.debug(`${currentState.state}`);
switch (currentState.state) {
case WorkflowCommandSelectionStateValue.WORKFLOW_JOB_SELECTION:
currentState = await workflowJobSelectionAction(currentState);
break;
case WorkflowCommandSelectionStateValue.WORKFLOW_RUN_SELECTION:
currentState = await workflowRunSelectionAction(currentState);
break;
case WorkflowCommandSelectionStateValue.WORKFLOW_STEP_SELECTION:
currentState = await workflowStepSelectionAction(currentState);
break;
}
}
return currentState;
};
@@ -0,0 +1,46 @@
import { WorkflowJobByIdQuery, WorkflowRunByIdWithJobsQuery } from '../../graphql/generated';
/*
* Utility types for workflow commands
*/
export enum WorkflowTriggerType {
MANUAL = 'Manual',
GITHUB = 'GitHub',
SCHEDULED = 'Scheduled',
OTHER = 'Other',
}
export type WorkflowRunResult = {
id: string;
status: string;
gitCommitMessage: string | null;
gitCommitHash: string | null;
triggerType: WorkflowTriggerType;
trigger: string | null;
startedAt: string;
finishedAt: string;
workflowId: string;
workflowName: string | null;
workflowFileName: string;
};
export type WorkflowResult = {
id: string;
name?: string | null | undefined;
fileName: string;
createdAt: string;
updatedAt: string;
};
export type WorkflowJobResult =
| WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number]
| WorkflowJobByIdQuery['workflowJobs']['byId'];
export type WorkflowRunWithJobsResult = WorkflowRunResult & {
jobs: WorkflowJobResult[];
logs?: string;
};
export type WorkflowLogLine = { time: string; msg: string; result?: string; marker?: string };
export type WorkflowLogs = Map<string, WorkflowLogLine[]>;
@@ -0,0 +1,128 @@
import { fetchRawLogsForJobAsync } from './fetchLogs';
import {
WorkflowJobResult,
WorkflowLogLine,
WorkflowLogs,
WorkflowRunResult,
WorkflowTriggerType,
} from './types';
import {
WorkflowRunFragment,
WorkflowRunStatus,
WorkflowRunTriggerEventType,
} from '../../graphql/generated';
import { Choice } from '../../prompts';
export function computeTriggerInfoForWorkflowRun(run: WorkflowRunFragment): {
triggerType: WorkflowTriggerType;
trigger: string | null;
} {
let triggerType = WorkflowTriggerType.OTHER;
let trigger = '';
if (run.actor?.__typename === 'Robot') {
if (run.actor.firstName?.startsWith('GitHub App · ')) {
trigger = `${run.requestedGitRef ?? ''}@${run.gitCommitHash?.substring(0, 12) ?? ''}`;
}
} else if (run.actor?.__typename === 'User') {
trigger = run.actor.username;
}
switch (run.triggerEventType) {
case WorkflowRunTriggerEventType.Manual:
triggerType = WorkflowTriggerType.MANUAL;
break;
case WorkflowRunTriggerEventType.GithubPullRequestLabeled:
case WorkflowRunTriggerEventType.GithubPullRequestOpened:
case WorkflowRunTriggerEventType.GithubPullRequestReopened:
case WorkflowRunTriggerEventType.GithubPullRequestSynchronize:
case WorkflowRunTriggerEventType.GithubPush:
triggerType = WorkflowTriggerType.GITHUB;
break;
case WorkflowRunTriggerEventType.Schedule:
triggerType = WorkflowTriggerType.SCHEDULED;
trigger = run.triggeringSchedule ?? '';
break;
}
return { triggerType, trigger };
}
export function choiceFromWorkflowRun(run: WorkflowRunResult): Choice {
const titleArray = [
run.workflowFileName,
run.status,
run.startedAt,
run.triggerType,
run.trigger,
];
return {
title: titleArray.join(' - '),
value: run.id,
description: `ID: ${run.id}, Message: ${run.gitCommitMessage?.split('\n')[0] ?? ''}`,
};
}
export function choiceFromWorkflowJob(job: WorkflowJobResult, index: number): Choice {
return {
title: `${job.name} - ${job.status}`,
value: index,
description: `ID: ${job.id}`,
};
}
export function choicesFromWorkflowLogs(logs: WorkflowLogs): Choice[] {
return Array.from(logs.keys()).map(step => {
const logLines = logs.get(step);
const stepStatus =
logLines?.filter((line: WorkflowLogLine) => line.marker === 'end-step')[0]?.result ?? '';
return {
title: `${step} - ${stepStatus}`,
value: step,
};
});
}
export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunResult[] {
return runs.map(run => {
const finishedAt = run.status === WorkflowRunStatus.InProgress ? null : run.updatedAt;
const { triggerType, trigger } = computeTriggerInfoForWorkflowRun(run);
return {
id: run.id,
status: run.status,
gitCommitMessage: run.gitCommitMessage?.split('\n')[0] ?? null,
gitCommitHash: run.gitCommitHash ?? null,
startedAt: run.createdAt,
finishedAt,
triggerType,
trigger,
workflowId: run.workflow.id,
workflowName: run.workflow.name ?? null,
workflowFileName: run.workflow.fileName,
};
});
}
export async function processLogsFromJobAsync(
job: WorkflowJobResult
): Promise<WorkflowLogs | null> {
const rawLogs = await fetchRawLogsForJobAsync(job);
if (!rawLogs) {
return null;
}
const logs: WorkflowLogs = new Map();
const logKeys = new Set<string>();
rawLogs.split('\n').forEach(line => {
try {
const parsedLine = JSON.parse(line);
const { result, marker } = parsedLine;
const { buildStepDisplayName, buildStepInternalId, time, msg } = parsedLine;
const stepId = buildStepDisplayName ?? buildStepInternalId;
if (stepId) {
if (!logKeys.has(stepId)) {
logKeys.add(stepId);
logs.set(stepId, []);
}
logs.get(stepId)?.push({ time, msg, result, marker });
}
} catch {}
});
return logs;
}
@@ -1,4 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -66,8 +67,13 @@ export default class BuildConfigure extends EasCommand {
nonInteractive: false,
vcsClient,
});
if (didCreateEasJson && isUsingEASUpdate(exp, projectId)) {
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
if (didCreateEasJson) {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
if (isUsingEASUpdate(exp, projectId, easJsonCliConfig.updateManifestHostOverride ?? null)) {
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
}
}
// configure expo-updates
+355
View File
@@ -0,0 +1,355 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonAccessor } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import {
createBuildProfileAsync,
doesBuildProfileExistAsync,
ensureProjectConfiguredAsync,
} from '../../build/configure';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import { downloadAndRunAsync, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { createFingerprintAsync } from '../../fingerprint/cli';
import { BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync, promptAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
import { Client } from '../../vcs/vcs';
const DEFAULT_EAS_BUILD_RUN_PROFILE_NAME = 'development-simulator';
export default class BuildDev extends EasCommand {
static override hidden: true;
static override description =
'run dev client simulator/emulator build with matching fingerprint or create a new one';
static override flags = {
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
char: 'p',
options: [Platform.IOS, Platform.ANDROID],
}),
profile: Flags.string({
char: 'e',
description: `Name of the build profile from eas.json. It must be a profile allowing to create emulator/simulator internal distribution dev client builds. The "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile will be selected by default.`,
helpValue: 'PROFILE_NAME',
}),
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
...this.ContextOptions.Analytics,
...this.ContextOptions.ProjectId,
};
protected override async runAsync(): Promise<any> {
const { flags } = await this.parse(BuildDev);
const {
loggedIn: { actor, graphqlClient },
getDynamicPrivateProjectConfigAsync,
projectDir,
analytics,
vcsClient,
projectId,
} = await this.getContextAsync(BuildDev, {
nonInteractive: false,
withServerSideEnvironment: null,
});
const platform = await this.selectPlatformAsync(flags.platform);
if (process.platform !== 'darwin' && platform === Platform.IOS) {
Errors.error('Running iOS builds in simulator is only supported on macOS.', { exit: 1 });
}
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: false,
vcsClient,
});
const buildProfile = await this.ensureValidBuildRunProfileExistsAsync({
projectDir,
platform,
selectedBuildProfileName: flags.profile,
vcsClient,
});
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
const { env } = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
const fingerprint = await createFingerprintAsync(projectDir, {
env,
workflow,
platforms: [platform],
});
if (!fingerprint) {
Errors.error('Failed to calculate fingerprint', { exit: 1 });
}
Log.log(`✨ Calculated fingerprint hash: ${fingerprint.hash}`);
Log.newLine();
const builds = await this.getBuildsAsync({
graphqlClient,
projectId,
platform,
fingerprint,
profile: buildProfile.profileName,
});
if (builds.length !== 0) {
const build = builds[0];
Log.succeed(
`🎯 Found successful build with matching fingerprint on EAS servers. Running it...`
);
if (build.artifacts?.applicationArchiveUrl) {
await downloadAndRunAsync(build);
await this.startDevServerAsync({ projectDir, platform });
return;
} else {
Log.warn('Artifacts for this build expired. New build will be started.');
}
}
Log.log('🚀 No successful build with matching fingerprint found. Starting a new build...');
const previousBuildsForSelectedProfile = await this.getBuildsAsync({
graphqlClient,
projectId,
platform,
profile: buildProfile.profileName,
});
if (
previousBuildsForSelectedProfile.length > 0 &&
previousBuildsForSelectedProfile[0].metrics?.buildDuration
) {
Log.log(
`🕒 Previous build for "${buildProfile.profileName}" profile completed in ${Math.floor(
previousBuildsForSelectedProfile[0].metrics.buildDuration / 60000
)} minutes.`
);
}
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flags: {
requestedPlatform:
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
nonInteractive: false,
freezeCredentials: false,
wait: true,
clearCache: false,
json: false,
autoSubmit: false,
localBuildOptions: {},
profile: flags.profile ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
},
actor,
getDynamicPrivateProjectConfigAsync,
downloadSimBuildAutoConfirm: true,
envOverride: env,
});
await this.startDevServerAsync({ projectDir, platform });
}
private async selectPlatformAsync(platform?: Platform): Promise<Platform> {
if (platform) {
return platform;
}
const { resolvedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'resolvedPlatform',
choices: [
{ title: 'Android', value: Platform.ANDROID },
{ title: 'iOS', value: Platform.IOS },
],
});
return resolvedPlatform;
}
private async validateBuildRunProfileAsync({
platform,
buildProfile,
buildProfileName,
}: {
platform: Platform;
buildProfile: BuildProfile;
buildProfileName: string;
}): Promise<void> {
if (buildProfile.developmentClient !== true) {
Errors.error(
`Profile "${buildProfileName}" must specify "developmentClient: true" to create a dev client build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
if (buildProfile.distribution !== 'internal') {
Errors.error(
`Profile "${buildProfileName}" must specify "distribution: internal" in order to work with eas build:dev command. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
if (platform === Platform.IOS) {
const iosProfile = buildProfile as BuildProfile<Platform.IOS>;
if (iosProfile.simulator !== true && iosProfile.withoutCredentials !== true) {
Errors.error(
`Profile "${buildProfileName}" must specify "ios.simulator: true" or "withoutCredentials: true" to create an iOS simulator build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
} else {
const androidProfile = buildProfile as BuildProfile<Platform.ANDROID>;
if (
androidProfile.distribution !== 'internal' &&
androidProfile.withoutCredentials !== true
) {
Errors.error(
`Profile "${buildProfileName}" must specify "distribution: internal" or "withoutCredentials: true" to create an Android emulator build. Select a different profile or update the profile in eas.json.`,
{ exit: 1 }
);
}
}
}
private async ensureValidBuildRunProfileExistsAsync({
projectDir,
platform,
selectedBuildProfileName,
vcsClient,
}: {
projectDir: string;
platform: Platform;
selectedBuildProfileName?: string;
vcsClient: Client;
}): Promise<ProfileData<'build'>> {
if (
!!selectedBuildProfileName ||
(await doesBuildProfileExistAsync({
projectDir,
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
}))
) {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const [buildProfile] = await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [platform],
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
projectDir,
});
await this.validateBuildRunProfileAsync({
buildProfileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
platform,
buildProfile: buildProfile.profile,
});
} else {
const createBuildProfile = await confirmAsync({
message: `We want to go ahead and generate "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile for you, that matches eas build:dev criteria. Do you want to proceed?`,
});
if (!createBuildProfile) {
Errors.error(
'Come back later or specify different build compliant with eas build:dev requirements by using "--profile" flag.',
{ exit: 1 }
);
}
await createBuildProfileAsync({
projectDir,
profileName: DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
profileContents: {
developmentClient: true,
distribution: 'internal',
ios: {
simulator: true,
},
environment: 'development',
},
nonInteractive: false,
vcsClient,
});
}
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const [buildProfile] = await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [platform],
profileName: selectedBuildProfileName ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
projectDir,
});
return buildProfile;
}
private async getBuildsAsync({
graphqlClient,
projectId,
platform,
fingerprint,
profile,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: Platform;
fingerprint?: { hash: string };
profile?: string;
}): Promise<BuildFragment[]> {
return await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
filter: {
platform: toAppPlatform(platform),
fingerprintHash: fingerprint?.hash,
status: BuildStatus.Finished,
simulator: platform === Platform.IOS ? true : undefined,
distribution: platform === Platform.ANDROID ? DistributionType.Internal : undefined,
developmentClient: true,
buildProfile: profile,
},
offset: 0,
limit: 1,
});
}
private async startDevServerAsync({
projectDir,
platform,
}: {
projectDir: string;
platform: Platform;
}): Promise<void> {
Log.newLine();
Log.log(
`Starting development server: ${chalk.dim(
`npx expo start --dev-client ${platform === Platform.IOS ? '--ios' : '--android'}`
)}`
);
await expoCommandAsync(projectDir, [
'start',
'--dev-client',
platform === Platform.IOS ? '--ios' : '--android',
]);
}
}
@@ -0,0 +1,166 @@
import { Platform } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import { pathExists } from 'fs-extra';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { getEasBuildRunCachedAppPath } from '../../run/run';
import { downloadAndMaybeExtractAppAsync } from '../../utils/download';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class Download extends EasCommand {
static override description = 'download simulator/emulator builds for a given fingerprint hash';
static override flags = {
fingerprint: Flags.string({
description: 'Fingerprint hash of the build to download',
required: true,
}),
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
char: 'p',
options: [Platform.IOS, Platform.ANDROID],
}),
'dev-client': Flags.boolean({
description: 'Filter only dev-client builds.',
allowNo: true,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectId,
};
async runAsync(): Promise<void> {
const {
flags: {
json: jsonFlag,
platform,
fingerprint,
'dev-client': developmentClient,
'non-interactive': nonInteractive,
},
} = await this.parse(Download);
const {
loggedIn: { graphqlClient },
projectId,
} = await this.getContextAsync(Download, {
nonInteractive,
});
if (jsonFlag) {
enableJsonOutput();
}
const selectedPlatform = await resolvePlatformAsync({ nonInteractive, platform });
const build = await this.getBuildAsync({
graphqlClient,
projectId,
platform: selectedPlatform,
fingerprintHash: fingerprint,
developmentClient,
});
const buildArtifactPath = await this.getPathToBuildArtifactAsync(build, selectedPlatform);
if (jsonFlag) {
const jsonResults = { path: buildArtifactPath };
printJsonOnlyOutput(jsonResults);
} else {
Log.log(`Build downloaded to ${chalk.bold(buildArtifactPath)}`);
}
}
private async getBuildAsync({
graphqlClient,
projectId,
platform,
fingerprintHash,
developmentClient,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: AppPlatform;
fingerprintHash: string;
developmentClient: boolean;
}): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
filter: {
platform,
fingerprintHash,
status: BuildStatus.Finished,
simulator: platform === AppPlatform.Ios ? true : undefined,
distribution: platform === AppPlatform.Android ? DistributionType.Internal : undefined,
developmentClient,
},
offset: 0,
limit: 1,
});
if (builds.length === 0) {
throw new Errors.CLIError(
`No builds available for ${platform} with fingerprint hash ${fingerprintHash}`
);
}
Log.succeed(`🎯 Found successful build with matching fingerprint on EAS servers.`);
return builds[0];
}
async getPathToBuildArtifactAsync(build: BuildFragment, platform: AppPlatform): Promise<string> {
const cachedBuildArtifactPath = getEasBuildRunCachedAppPath(
build.project.id,
build.id,
platform
);
if (await pathExists(cachedBuildArtifactPath)) {
Log.newLine();
Log.log(`Using cached build...`);
return cachedBuildArtifactPath;
}
if (!build.artifacts?.applicationArchiveUrl) {
throw new Error('Build does not have an application archive url');
}
return await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
platform,
cachedBuildArtifactPath
);
}
}
async function resolvePlatformAsync({
nonInteractive,
platform,
}: {
nonInteractive: boolean;
platform?: Platform;
}): Promise<AppPlatform> {
if (nonInteractive && !platform) {
throw new Error('Platform must be provided in non-interactive mode');
}
if (platform) {
return toAppPlatform(platform);
}
const { selectedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'selectedPlatform',
choices: [
{ title: 'Android', value: AppPlatform.Android },
{ title: 'iOS', value: AppPlatform.Ios },
],
});
return selectedPlatform;
}
+76 -11
View File
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import { BuildProfile, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -17,6 +17,7 @@ import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform'
import { selectAsync } from '../../prompts';
import uniq from '../../utils/expodash/uniq';
import { enableJsonOutput } from '../../utils/json';
import { ProfileData } from '../../utils/profiles';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
interface RawBuildFlags {
@@ -36,7 +37,8 @@ interface RawBuildFlags {
message?: string;
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
repack: boolean;
'verbose-logs'?: boolean;
'what-to-test'?: string;
}
export default class Build extends EasCommand {
@@ -78,16 +80,22 @@ export default class Build extends EasCommand {
description: 'Clear cache before the build',
}),
'auto-submit': Flags.boolean({
char: 's',
default: false,
description:
'Submit on build complete using the submit profile with the same name as the build profile',
exclusive: ['auto-submit-with-profile'],
aliases: ['submit'],
}),
'auto-submit-with-profile': Flags.string({
description: 'Submit on build complete using the submit profile with provided name',
helpValue: 'PROFILE_NAME',
exclusive: ['auto-submit'],
}),
'what-to-test': Flags.string({
description:
'Specify the "What to Test" information for the build in TestFlight (iOS-only). To be used with the `auto-submit` flag',
}),
'resource-class': Flags.enum({
options: Object.values(ResourceClass),
hidden: true,
@@ -110,10 +118,9 @@ export default class Build extends EasCommand {
default: false,
description: 'Prevent the build from updating credentials in non-interactive mode',
}),
repack: Flags.boolean({
'verbose-logs': Flags.boolean({
default: false,
hidden: true,
description: 'Use the golden dev client build repack flow as it works for onboarding',
description: 'Use verbose logs for the build process',
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -158,15 +165,20 @@ export default class Build extends EasCommand {
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
await runBuildAndSubmitAsync(
const { buildProfiles } = await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flagsWithPlatform,
flags: flagsWithPlatform,
actor,
getDynamicPrivateProjectConfigAsync
);
getDynamicPrivateProjectConfigAsync,
});
this.maybeSuggestUsingEasBuildDev({
buildProfiles,
nonInteractive: flags.nonInteractive,
});
}
private sanitizeFlags(
@@ -182,6 +194,13 @@ export default class Build extends EasCommand {
if (flags.json && !nonInteractive) {
Errors.error('--json is allowed only when building in non-interactive mode', { exit: 1 });
}
const autoSubmit = flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined;
if (flags['what-to-test'] && !autoSubmit) {
Errors.error(
'The --what-to-test flag can only be used with the --auto-submit or --auto-submit-with-profile flag.',
{ exit: 1 }
);
}
const requestedPlatform =
flags.platform &&
@@ -224,13 +243,14 @@ export default class Build extends EasCommand {
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
autoSubmit,
submitProfile: flags['auto-submit-with-profile'] ?? profile,
resourceClass: flags['resource-class'],
message,
buildLoggerLevel: flags['build-logger-level'],
freezeCredentials: flags['freeze-credentials'],
repack: flags.repack,
isVerboseLoggingEnabled: flags['verbose-logs'],
whatToTest: flags['what-to-test'],
};
}
@@ -266,6 +286,51 @@ export default class Build extends EasCommand {
requestedPlatform,
};
}
private maybeSuggestUsingEasBuildDev({
buildProfiles,
nonInteractive,
}: {
buildProfiles: ProfileData<'build'>[] | undefined;
nonInteractive: boolean;
}): void {
// suggest using eas build:dev if the build configuration results in simulator/emulator dev client build
if (
!nonInteractive &&
!process.env.CI &&
buildProfiles?.some(({ profile, platform }) => {
if (profile.developmentClient !== true) {
return false;
}
if (profile.distribution !== 'internal') {
return false;
}
if (platform === Platform.IOS) {
const iosProfile = profile as BuildProfile<Platform.IOS>;
if (iosProfile.simulator !== true && iosProfile.withoutCredentials !== true) {
return false;
}
} else {
const androidProfile = profile as BuildProfile<Platform.ANDROID>;
if (
androidProfile.distribution !== 'internal' &&
androidProfile.withoutCredentials !== true
) {
return false;
}
}
return true;
})
) {
Log.newLine();
Log.log(
`🔎 TIP: You are using a build configuration that could benefit from using ${chalk.bold(
'eas build:dev'
)} command. Run it to install and run cached development build, or create a new one if a compatible build doesn't exist yet.`
);
}
}
}
export async function handleDeprecatedEasJsonAsync(
@@ -99,12 +99,12 @@ export default class BuildInspect extends EasCommand {
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
} else {
try {
await runBuildAndSubmitAsync(
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
{
flags: {
nonInteractive: false,
freezeCredentials: false,
wait: true,
@@ -121,11 +121,10 @@ export default class BuildInspect extends EasCommand {
workingdir: tmpWorkingdir,
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
},
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync
);
getDynamicPrivateProjectConfigAsync,
});
if (!flags.verbose) {
Log.log(chalk.green('Build successful'));
}
@@ -6,8 +6,7 @@ import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import { RequestedPlatform } from '../../platform';
import { enableJsonOutput } from '../../utils/json';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
import NoVcsClient from '../../vcs/clients/noVcs';
import GitClient from '../../vcs/clients/git';
/**
* This command will be run on the EAS Build workers, when building
@@ -64,18 +63,24 @@ export default class BuildInternal extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildInternal, {
nonInteractive: true,
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
withServerSideEnvironment: null,
});
if (vcsClient instanceof GitClient) {
// `build:internal` is run on EAS workers and the repo may have been changed
// by pre-install hooks or other scripts. We don't want to require committing changes
// to continue the build.
vcsClient.requireCommit = false;
}
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync(
await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
{
flags: {
requestedPlatform: flags.platform as RequestedPlatform,
profile: flags.profile,
nonInteractive: true,
@@ -88,10 +93,9 @@ export default class BuildInternal extends EasCommand {
localBuildMode: LocalBuildMode.INTERNAL,
},
submitProfile: flags['auto-submit-with-profile'] ?? flags.profile,
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync
);
getDynamicPrivateProjectConfigAsync,
});
}
}
@@ -62,6 +62,9 @@ export default class BuildList extends EasCommand {
aliases: ['gitCommitHash'],
description: 'Filter only builds created with the specified git commit hash',
}),
'fingerprint-hash': Flags.string({
description: 'Filter only builds with the specified fingerprint hash',
}),
...EasPaginatedQueryFlags,
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: BUILDS_LIMIT }),
...EasNonInteractiveAndJsonFlags,
@@ -130,6 +133,7 @@ export default class BuildList extends EasCommand {
buildProfile: flags['build-profile'],
gitCommitHash: flags['git-commit-hash'],
simulator: flags.simulator,
fingerprintHash: flags['fingerprint-hash'],
},
paginatedQueryOptions,
});
+1 -14
View File
@@ -1,7 +1,6 @@
import { Errors, Flags } from '@oclif/core';
import assert from 'assert';
import { pathExists } from 'fs-extra';
import path from 'path';
import { getLatestBuildAsync, listAndSelectBuildOnAppAsync } from '../../build/queries';
import EasCommand from '../../commandUtils/EasCommand';
@@ -17,13 +16,12 @@ import Log from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { RunArchiveFlags, runAsync } from '../../run/run';
import { RunArchiveFlags, getEasBuildRunCachedAppPath, runAsync } from '../../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../../run/utils';
import {
downloadAndMaybeExtractAppAsync,
extractAppFromLocalArchiveAsync,
} from '../../utils/download';
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
interface RawRunFlags {
latest?: boolean;
@@ -245,17 +243,6 @@ async function maybeGetBuildAsync(
}
}
function getEasBuildRunCachedAppPath(
projectId: string,
buildId: string,
platform: AppPlatform
): string {
return path.join(
getEasBuildRunCacheDirectoryPath(),
`${projectId}_${buildId}.${platform === AppPlatform.Ios ? 'app' : 'apk'}`
);
}
async function getPathToSimulatorBuildAppAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
@@ -120,7 +120,7 @@ export default class BuildVersionSyncView extends EasCommand {
const workflow = await resolveWorkflowAsync(projectDir, profileInfo.platform, vcsClient);
if (!remoteVersions?.buildVersion) {
Log.warn(
`Skipping versions sync for ${platformDisplayName}. There are no versions configured on Expo servers, use "eas build:version:set" or run a build to initialize it.`
`Skipping versions sync for ${platformDisplayName}. There are no versions configured on EAS, use "eas build:version:set" or run a build to initialize it.`
);
continue;
}
@@ -52,7 +52,7 @@ export default class ChannelDelete extends EasCommand {
let channelId, channelName;
if (nameArg) {
const { id, name } = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
const { id, name } = await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
appId: projectId,
channelName: nameArg,
});
@@ -94,7 +94,7 @@ export default class ChannelEdit extends EasCommand {
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
@@ -86,7 +86,7 @@ export default class ChannelPause extends EasCommand {
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
@@ -86,7 +86,7 @@ export default class ChannelResume extends EasCommand {
}
const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
? await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
appId: projectId,
channelName: args.name,
})
@@ -0,0 +1,142 @@
import chalk from 'chalk';
import EasCommand from '../../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../../commandUtils/flags';
import Log from '../../../log';
import { Ora, ora } from '../../../ora';
import { toggleConfirmAsync } from '../../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
import {
deleteWorkerDeploymentAliasAsync,
selectWorkerDeploymentAliasOnAppAsync,
} from '../../../worker/deployment';
interface DeployAliasDeleteFlags {
nonInteractive: boolean;
json: boolean;
}
export default class WorkerAliasDelete extends EasCommand {
static override description = 'Delete deployment aliases.';
static override aliases = ['worker:alias:delete'];
static override state = 'preview';
static override args = [{ name: 'ALIAS_NAME' }];
static override flags = {
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
override async runAsync(): Promise<void> {
const {
args: { ALIAS_NAME: aliasNameFromArg },
flags: rawFlags,
} = await this.parse(WorkerAliasDelete);
const flags = this.sanitizeFlags(rawFlags);
if (flags.json) {
enableJsonOutput();
}
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkerAliasDelete, {
nonInteractive: true,
withServerSideEnvironment: null,
});
const { projectId } = await getDynamicPrivateProjectConfigAsync();
const aliasName = await resolveDeploymentAliasAsync(
flags,
graphqlClient,
projectId,
aliasNameFromArg
);
const isProduction = !aliasName;
if (!flags.nonInteractive) {
Log.addNewLineIfNone();
Log.warn(
isProduction
? `You are about to delete your production alias`
: `You are about to delete alias: "${aliasName}"`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.log('Aborted.');
return;
}
}
let progress: null | Ora = null;
let deleteResult: null | Awaited<ReturnType<typeof deleteWorkerDeploymentAliasAsync>> = null;
try {
progress = ora(chalk`Deleting alias {bold ${aliasName ?? 'production'}}`).start();
deleteResult = await deleteWorkerDeploymentAliasAsync({
graphqlClient,
appId: projectId,
aliasName,
});
progress.text = chalk`Deleted alias {bold ${aliasName ?? 'production'}}`;
} catch (error: any) {
progress?.fail(chalk`Failed to delete alias {bold ${aliasName ?? 'production'}}`);
throw error;
}
progress?.succeed(chalk`Deleted alias {bold ${aliasName ?? 'production'}}`);
if (flags.json) {
printJsonOnlyOutput({
aliasName: deleteResult.aliasName,
id: deleteResult.id,
});
}
}
private sanitizeFlags(flags: any): DeployAliasDeleteFlags {
return {
nonInteractive: flags['non-interactive'],
json: flags['json'],
};
}
}
async function resolveDeploymentAliasAsync(
flags: DeployAliasDeleteFlags,
graphqlClient: ExpoGraphqlClient,
projectId: string,
aliasNameFromArg?: string
): Promise<string> {
if (aliasNameFromArg?.trim()) {
return aliasNameFromArg.trim().toLowerCase();
}
if (flags.nonInteractive) {
throw new Error('Alias name must be provided in non-interactive mode');
}
const alias = await selectWorkerDeploymentAliasOnAppAsync({
graphqlClient,
appId: projectId,
selectTitle: 'alias to delete',
});
if (!alias) {
throw new Error('No aliases found for this project, create aliases with "eas deploy:alias"');
}
return alias.aliasName;
}
@@ -1,20 +1,23 @@
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 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';
} from '../../../worker/deployment';
import {
formatWorkerDeploymentJson,
formatWorkerDeploymentTable,
} from '../../../worker/utils/logs';
interface DeployAliasFlags {
nonInteractive: boolean;
@@ -33,12 +36,9 @@ interface RawDeployAliasFlags {
}
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 description = 'Assign deployment aliases.';
static override aliases = ['worker:alias', 'deploy:promote'];
static override state = 'preview';
static override flags = {
prod: Flags.boolean({
@@ -73,7 +73,7 @@ export default class WorkerAlias extends EasCommand {
enableJsonOutput();
}
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
@@ -0,0 +1,114 @@
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import Log from '../../log';
import { Ora, ora } from '../../ora';
import { toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { deleteWorkerDeploymentAsync } from '../../worker/deployment';
interface DeployDeleteFlags {
nonInteractive: boolean;
json: boolean;
}
interface RawDeployDeleteFlags {
'non-interactive': boolean;
json: boolean;
}
export default class WorkerDelete extends EasCommand {
static override description = 'Delete a deployment.';
static override aliases = ['worker:delete'];
static override state = 'preview';
static override args = [{ name: 'DEPLOYMENT_ID' }];
static override flags = {
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
override async runAsync(): Promise<void> {
const {
args: { DEPLOYMENT_ID: deploymentIdFromArg },
flags: rawFlags,
} = await this.parse(WorkerDelete);
const flags = this.sanitizeFlags(rawFlags);
if (flags.json) {
enableJsonOutput();
}
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkerDelete, {
nonInteractive: true,
withServerSideEnvironment: null,
});
const { projectId } = await getDynamicPrivateProjectConfigAsync();
if (!deploymentIdFromArg) {
if (flags.nonInteractive) {
throw new Error('Deployment ID must be provided in non-interactive mode');
}
throw new Error('Deployment ID is required');
}
if (!flags.nonInteractive) {
Log.addNewLineIfNone();
Log.warn(
`You are about to permanently delete deployment with ID: "${deploymentIdFromArg}"` +
`\nThis action is irreversible.`
);
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
if (!confirmed) {
Log.log('Aborted.');
return;
}
}
let progress: Ora | null = null;
let deleteResult: null | Awaited<ReturnType<typeof deleteWorkerDeploymentAsync>> = null;
try {
progress = ora(chalk`Deleting deployment {bold ${deploymentIdFromArg}}`).start();
deleteResult = await deleteWorkerDeploymentAsync({
graphqlClient,
appId: projectId,
deploymentIdentifier: deploymentIdFromArg,
});
progress.text = chalk`Deleted deployment {bold ${deploymentIdFromArg}}`;
} catch (error: any) {
progress?.fail(chalk`Failed to delete deployment {bold ${deploymentIdFromArg}}`);
throw error;
}
progress?.succeed(chalk`Deleted deployment {bold ${deploymentIdFromArg}}`);
if (flags.json) {
printJsonOnlyOutput({
deploymentId: deleteResult.deploymentIdentifier,
id: deleteResult.id,
});
}
}
private sanitizeFlags(flags: RawDeployDeleteFlags): DeployDeleteFlags {
return {
nonInteractive: flags['non-interactive'],
json: flags['json'],
};
}
}
@@ -1,16 +1,18 @@
import { format as formatTimeAgo } from '@expo/timeago.js';
import { Flags } from '@oclif/core';
import { CombinedError } from '@urql/core';
import chalk from 'chalk';
import fs from 'node:fs';
import * as path from 'node:path';
import { getHostingDeploymentsUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import Log from '../../log';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { createProgressTracker } from '../../utils/progress';
import * as WorkerAssets from '../../worker/assets';
import {
assignWorkerDeploymentAliasAsync,
@@ -18,9 +20,10 @@ import {
getSignedDeploymentUrlAsync,
} from '../../worker/deployment';
import {
UploadParams,
UploadPayload,
batchUploadAsync,
callUploadApiAsync,
createProgressBar,
uploadAsync,
} from '../../worker/upload';
import {
@@ -29,6 +32,8 @@ import {
getDeploymentUrlFromFullName,
} from '../../worker/utils/logs';
const MAX_UPLOAD_SIZE = 5e8; // 500MB
const isDirectory = (directoryPath: string): Promise<boolean> =>
fs.promises
.stat(directoryPath)
@@ -57,21 +62,23 @@ interface RawDeployFlags {
'dry-run': boolean;
}
interface UploadAssetBatchInstruction {
sha512: string[];
}
interface DeployInProgressParams {
id: string;
fullName: string;
baseURL: string;
token: string;
upload?: UploadAssetBatchInstruction[];
}
export default class WorkerDeploy extends EasCommand {
static override description = 'Deploy your Expo web build';
static override aliases = ['deploy'];
static override description = 'deploy your Expo Router web build and API Routes';
static override aliases = ['worker: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 state = 'preview';
static override flags = {
prod: Flags.boolean({
@@ -114,7 +121,7 @@ export default class WorkerDeploy extends EasCommand {
enableJsonOutput();
}
Log.warn('EAS Worker Deployments are in beta and subject to breaking changes.');
Log.warn('EAS Hosting is still in preview and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
@@ -123,7 +130,10 @@ export default class WorkerDeploy extends EasCommand {
} = await this.getContextAsync(WorkerDeploy, { ...flags, withServerSideEnvironment: null });
const projectDist = await resolveExportedProjectAsync(flags, projectDir);
const { projectId } = await getDynamicPrivateProjectConfigAsync();
const { projectId, exp } = await getDynamicPrivateProjectConfigAsync();
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
logExportedProjectInfo(projectDist);
@@ -159,14 +169,14 @@ export default class WorkerDeploy extends EasCommand {
tarPath: string,
uploadUrl: string
): Promise<DeployInProgressParams> {
const { response } = await uploadAsync({
url: uploadUrl,
filePath: tarPath,
compress: false,
headers: {
accept: 'application/json',
const payload = { filePath: tarPath };
const { response } = await uploadAsync(
{
baseURL: uploadUrl,
method: 'POST',
},
});
payload
);
if (response.status === 413) {
throw new Error(
'Upload failed! (Payload too large)\n' +
@@ -182,78 +192,84 @@ export default class WorkerDeploy extends EasCommand {
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;
const { id, fullName, token, upload } = 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');
} else if (!Array.isArray(upload) && upload !== undefined) {
throw new Error('Upload failed: API returned invalid asset upload instructions');
}
const baseURL = new URL('/', uploadUrl).toString();
return { id, fullName, baseURL, token };
return { id, fullName, baseURL, token, upload };
}
}
async function uploadAssetsAsync(
assetMap: WorkerAssets.AssetMap,
assetFiles: WorkerAssets.AssetFileEntry[],
deployParams: DeployInProgressParams
): Promise<void> {
const uploadParams: UploadParams[] = [];
const assetPath = projectDist.type === 'server' ? projectDist.clientPath : projectDist.path;
if (!assetPath) {
return;
const baseURL = new URL('/asset/', deployParams.baseURL);
const uploadInit = { baseURL, method: 'POST' };
uploadInit.baseURL.searchParams.set('token', deployParams.token);
const uploadPayloads: UploadPayload[] = [];
if (deployParams.upload) {
const assetsBySHA512 = assetFiles.reduce((map, asset) => {
map.set(asset.sha512, asset);
return map;
}, new Map<string, WorkerAssets.AssetFileEntry>());
const payloads = deployParams.upload
.map(instruction =>
instruction.sha512.map(sha512 => {
const asset = assetsBySHA512.get(sha512);
if (!asset) {
// NOTE(@kitten): This should never happen
throw new Error(
`Uploading assets failed: API instructed us to upload an asset that does not exist`
);
}
return asset;
})
)
.filter(assets => assets && assets.length > 0)
.map(assets => (assets.length > 1 ? { multipart: assets } : { asset: assets[0] }));
uploadPayloads.push(...payloads);
} else {
// NOTE(@kitten): Legacy format which uploads assets one-by-one
uploadPayloads.push(...assetFiles.map(asset => ({ asset })));
}
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',
});
const progressTotal = uploadPayloads.reduce(
(acc, payload) => acc + ('multipart' in payload ? payload.multipart.length : 1),
0
);
const progressTracker = createProgressBar(`Uploading ${progressTotal} 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 });
for await (const signal of batchUploadAsync(
uploadInit,
uploadPayloads,
progressTracker.update
)) {
progressTracker.update(signal.progress);
}
} catch (error: any) {
updateProgress({ isComplete: true, error });
progressTracker.stop();
throw error;
} finally {
progressTracker.stop();
}
updateProgress({ isComplete: true });
}
let assetMap: WorkerAssets.AssetMap;
let tarPath: string;
let assetFiles: WorkerAssets.AssetFileEntry[];
let deployResult: DeployInProgressParams;
let progress = ora('Preparing project').start();
try {
const manifest = await WorkerAssets.createManifestAsync(
const manifestResult = await WorkerAssets.createManifestAsync(
{
environment: flags.environment,
projectDir,
@@ -261,13 +277,21 @@ export default class WorkerDeploy extends EasCommand {
},
graphqlClient
);
assetMap = await WorkerAssets.createAssetMapAsync(
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path
if (manifestResult.conflictingVariableNames?.length) {
Log.warn(
'> The following environment variables were present in local .env files as well as EAS environment variables. ' +
'In case of conflict, the EAS environment variable values will be used: ' +
manifestResult.conflictingVariableNames.join(' ')
);
}
assetFiles = await WorkerAssets.collectAssetsAsync(
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path,
{ maxFileSize: MAX_UPLOAD_SIZE }
);
tarPath = await WorkerAssets.packFilesIterableAsync(
emitWorkerTarballAsync({
assetMap,
manifest,
assetMap: WorkerAssets.assetsToAssetsMap(assetFiles),
manifest: manifestResult.manifest,
})
);
@@ -295,10 +319,36 @@ export default class WorkerDeploy extends EasCommand {
progress.succeed('Created deployment');
} catch (error: any) {
progress.fail('Failed to create deployment');
if (
flags.isProduction &&
error instanceof CombinedError &&
error.graphQLErrors.some(err => {
return (
err.extensions?.errorCode === 'UNAUTHORIZED_ERROR' &&
err.message.includes('AppDevDomainNameEntity') &&
err.message.includes('CREATE')
);
})
) {
throw new Error(
`You have specified the new deployment should be a production deployment, but a production domain has not been set up yet for this app.\n\nRun ${chalk.bold(
'eas deploy --prod'
)} as an app admin on your machine or promote an existing deployment to production on the ${link(
getHostingDeploymentsUrl(accountName, projectName),
{
dim: false,
text: 'Hosting Dashboard',
fallback: `Hosting Dashboard (${getHostingDeploymentsUrl(accountName, projectName)})`,
}
)} to set up a production domain and then try again.`
);
}
throw error;
}
await uploadAssetsAsync(assetMap, deployResult);
await uploadAssetsAsync(assetFiles, deployResult);
await finalizeDeployAsync(deployResult);
let deploymentAlias: null | Awaited<ReturnType<typeof assignWorkerDeploymentAliasAsync>> = null;
@@ -426,7 +476,7 @@ async function resolveExportedProjectAsync(
if (!exportDirStat?.isDirectory()) {
throw new Error(
`No "${flags.exportDir}/" folder found. Prepare your project for deployment with "npx expo export --platform web"`
`No "${flags.exportDir}/" folder found. Export your app with "npx expo export --platform web"`
);
}
@@ -451,7 +501,8 @@ function logExportedProjectInfo(
// 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.warn(`> Project export: ${project.type}${modifiedAgo}`);
} else {
Log.log(chalk`{dim > Project export: ${project.type}}`);
}
Log.log(chalk`{dim > Project export: ${project.type}${modifiedAgo}}`);
}
@@ -147,10 +147,10 @@ export default class DeviceDelete extends EasCommand {
graphqlClient: ExpoGraphqlClient,
chosenDevice: AppleDevice | AppleDeviceQueryResult
): Promise<void> {
const removalSpinner = ora(`Removing Apple device on Expo`).start();
const removalSpinner = ora(`Removing Apple device on EAS`).start();
try {
await AppleDeviceMutation.deleteAppleDeviceAsync(graphqlClient, chosenDevice.id);
removalSpinner.succeed('Removed Apple device from Expo');
removalSpinner.succeed('Removed Apple device from EAS');
} catch (err) {
removalSpinner.fail();
throw err;
@@ -105,12 +105,12 @@ export default class DeviceRename extends EasCommand {
chosenDevice: AppleDevice | AppleDeviceQueryResult,
newDeviceName: string
): Promise<void> {
const removalSpinner = ora(`Renaming Apple device on Expo`).start();
const removalSpinner = ora(`Renaming Apple device on EAS`).start();
try {
await AppleDeviceMutation.updateAppleDeviceAsync(graphqlClient, chosenDevice.id, {
name: newDeviceName,
});
removalSpinner.succeed('Renamed Apple device on Expo');
removalSpinner.succeed('Renamed Apple device on EAS');
} catch (err) {
removalSpinner.fail();
throw err;
@@ -289,7 +289,7 @@ describe(EnvCreate, () => {
});
});
it('creates an account-wide variable and links it', async () => {
it('creates an account-wide variable', async () => {
const command = new EnvCreate(
[
'--name',
@@ -302,7 +302,6 @@ describe(EnvCreate, () => {
'development',
'--scope',
'account',
'--link',
],
mockConfig
);
@@ -330,18 +329,6 @@ describe(EnvCreate, () => {
},
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 () => {
@@ -1,271 +0,0 @@
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 EnvLink from '../link';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvLink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
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 an account-wide 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 EnvLink(
['--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 an account-wide 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 EnvLink(
['--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 EnvLink(
[
'--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 EnvLink([], 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 EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide 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 EnvLink([], 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 EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -1,226 +0,0 @@
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 EnvUnlink from '../unlink';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvUnlink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
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 an account-wide 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 EnvUnlink(
['--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 an account-wide 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 EnvUnlink(
['--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 EnvUnlink([], 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 EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide 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 EnvUnlink([], 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 EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
+1 -61
View File
@@ -33,12 +33,11 @@ import {
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { isEnvironment, performForEnvironmentsAsync } from '../../utils/variableUtils';
import { isEnvironment } from '../../utils/variableUtils';
interface RawCreateFlags {
name?: string;
value?: string;
link?: boolean;
force?: boolean;
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
@@ -50,7 +49,6 @@ interface RawCreateFlags {
interface CreateFlags {
name?: string;
value?: string;
link?: boolean;
force?: boolean;
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
@@ -78,10 +76,6 @@ export default class EnvCreate extends EasCommand {
value: Flags.string({
description: 'Text value or the variable',
}),
link: Flags.boolean({
description: 'Link account-wide variable to the current project',
hidden: true, // every account-wide variable is global for now so it's not user facing
}),
force: Flags.boolean({
description: 'Overwrite existing variable',
default: false,
@@ -114,7 +108,6 @@ export default class EnvCreate extends EasCommand {
'non-interactive': nonInteractive,
environment: environments,
visibility,
link,
force,
type,
fileName,
@@ -154,29 +147,6 @@ export default class EnvCreate extends EasCommand {
});
overwrite = true;
}
if (existingVariable.scope === EnvironmentVariableScope.Shared) {
await this.promptForOverwriteAsync({
nonInteractive,
force,
message: `Account-wide variable with ${name} name already exists on this account.`,
suggestion: 'Do you want to unlink it first?',
});
Log.withTick(
`Unlinking account-wide variable ${chalk.bold(name)} on project ${chalk.bold(
projectDisplayName
)}.`
);
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
existingVariable.id,
projectId,
environment
);
});
}
}
const variable =
@@ -265,29 +235,6 @@ export default class EnvCreate extends EasCommand {
Log.withTick(
`Created a new variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
);
if (link) {
Log.withTick(
`Linking account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
variable.id,
projectId,
environment
);
});
Log.withTick(
`Linked account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
}
}
}
@@ -402,7 +349,6 @@ export default class EnvCreate extends EasCommand {
value,
environment: newEnvironments,
visibility: newVisibility,
link: rest.link ?? false,
force: rest.force ?? false,
'non-interactive': nonInteractive,
type: newType,
@@ -412,12 +358,6 @@ export default class EnvCreate extends EasCommand {
}
private sanitizeFlags(flags: RawCreateFlags): CreateFlags {
if (flags.scope !== 'account' && flags.link) {
throw new Error(
`Unexpected argument: --link can only be used when creating account-wide variables`
);
}
return {
...flags,
scope:
+70 -28
View File
@@ -53,9 +53,10 @@ export default class EnvExec extends EasCommand {
},
];
private isNonInteractive: boolean = false;
async runAsync(): Promise<void> {
const { flags, args } = await this.parse(EnvExec);
const parsedFlags = this.sanitizeFlagsAndArgs(flags, args);
const {
@@ -65,6 +66,8 @@ export default class EnvExec extends EasCommand {
nonInteractive: parsedFlags.nonInteractive,
});
this.isNonInteractive = parsedFlags.nonInteractive;
const environment =
parsedFlags.environment ??
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
@@ -72,12 +75,20 @@ export default class EnvExec extends EasCommand {
graphqlClient,
projectId,
environment,
nonInteractive: parsedFlags.nonInteractive,
});
await this.runCommandWithEnvVarsAsync({
command: parsedFlags.command,
environmentVariables,
});
if (parsedFlags.nonInteractive) {
await this.runCommandNonInteractiveWithEnvVarsAsync({
command: parsedFlags.command,
environmentVariables,
});
} else {
await this.runCommandWithEnvVarsAsync({
command: parsedFlags.command,
environmentVariables,
});
}
}
private sanitizeFlagsAndArgs(
@@ -96,13 +107,48 @@ export default class EnvExec extends EasCommand {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const firstChar = bash_command[0];
const lastChar = bash_command[bash_command.length - 1];
const cleanCommand =
(firstChar === '"' && lastChar === '"') || (firstChar === "'" && lastChar === "'")
? bash_command.slice(1, -1)
: bash_command;
return {
nonInteractive: rawFlags['non-interactive'],
environment,
command: bash_command,
command: cleanCommand,
};
}
// eslint-disable-next-line async-protect/async-suffix
protected override async catch(err: Error): Promise<any> {
// when in non-interactive, make the behavior of this command a pure passthrough, outputting only the subprocess' stdout and stderr and exiting with the
// sub-command's exit status
if (this.isNonInteractive) {
process.exitCode = process.exitCode ?? (err as any).status ?? 1;
} else {
await super.catch(err);
}
}
private async runCommandNonInteractiveWithEnvVarsAsync({
command,
environmentVariables,
}: {
command: string;
environmentVariables: Record<string, string>;
}): Promise<void> {
await spawnAsync(command, [], {
shell: true,
stdio: 'inherit',
env: {
...process.env,
...environmentVariables,
},
});
}
private async runCommandWithEnvVarsAsync({
command,
environmentVariables,
@@ -111,7 +157,8 @@ export default class EnvExec extends EasCommand {
environmentVariables: Record<string, string>;
}): Promise<void> {
Log.log(`Running command: ${chalk.bold(command)}`);
const spawnPromise = spawnAsync('bash', ['-c', command], {
const spawnPromise = spawnAsync(command, [], {
shell: true,
stdio: ['inherit', 'pipe', 'pipe'],
env: {
...process.env,
@@ -147,10 +194,12 @@ export default class EnvExec extends EasCommand {
graphqlClient,
projectId,
environment,
nonInteractive,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
environment: EnvironmentVariableEnvironment;
nonInteractive: boolean;
}): Promise<Record<string, string>> {
const environmentVariablesQueryResult =
await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
@@ -158,31 +207,24 @@ export default class EnvExec extends EasCommand {
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.');
if (!nonInteractive) {
if (nonSecretEnvironmentVariables.length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${nonSecretEnvironmentVariables
.map(e => e.name)
.join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
);
}
Log.newLine();
}
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) {
-204
View File
@@ -1,204 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
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, isEnvironment } from '../../utils/variableUtils';
type LinkFlags = {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
environment?: EnvironmentVariableEnvironment[];
};
export default class EnvLink extends EasCommand {
static override description = 'link an account-wide environment variable to the current project';
// for now we only roll out global account-wide env variables so this should stay hidden
static override hidden = true;
static override flags = {
'variable-name': Flags.string({
description: 'Name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to link',
}),
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
static override args = [
{
name: 'environment',
description:
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
required: false,
},
];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvLink);
let {
'variable-name': name,
'variable-environment': currentEnvironment,
'non-interactive': nonInteractive,
environment: environments,
} = this.validateInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvLink, {
nonInteractive,
});
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment: currentEnvironment,
filterNames: name ? [name] : undefined,
});
let selectedVariable = variables[0];
if (variables.length > 1) {
if (nonInteractive) {
throw new Error(
'Multiple variables found, run command with --variable-name and --variable-environment arguments.'
);
}
selectedVariable = await selectAsync(
'Select account-wide variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
}
if (!selectedVariable) {
throw new Error(`Account-wide variable ${name} doesn't exist`);
}
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;
}
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;
}
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);
}
}
}
private validateInputs(flags: LinkFlags, { environment }: { environment?: string }): LinkFlags {
environment = environment?.toUpperCase();
if (environment && !isEnvironment(environment)) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const environments = flags.environment
? flags.environment
: environment
? [environment as EnvironmentVariableEnvironment]
: undefined;
if (flags['non-interactive']) {
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(
'--variable-name VARIABLE_NAME'
)} flag to fix the issue`
);
}
}
return {
...flags,
environment: environments,
};
}
}
-192
View File
@@ -1,192 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
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, isEnvironment } from '../../utils/variableUtils';
type UnlinkFlags = {
'variable-name'?: string;
'non-interactive': boolean;
environment?: EnvironmentVariableEnvironment[];
};
export default class EnvUnlink extends EasCommand {
static override description =
'unlink an account-wide environment variable from the current project';
// for now we only roll out global account-wide env variables so this should stay hidden
static override hidden = true;
static override flags = {
'variable-name': Flags.string({
description: 'Name of the variable',
}),
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
static override args = [
{
name: 'environment',
description:
"Environment to unlink the variable from. One of 'production', 'preview', or 'development'.",
required: false,
},
];
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvUnlink);
let {
'variable-name': name,
'non-interactive': nonInteractive,
environment: unlinkEnvironments,
} = this.validateInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvUnlink, {
nonInteractive,
});
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
filterNames: name ? [name] : undefined,
});
let selectedVariable = variables[0];
if (variables.length > 1) {
if (nonInteractive) {
throw new Error("Multiple variables found, please select one using '--variable-name'");
}
selectedVariable = await selectAsync(
'Select account-wide variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
}
if (!selectedVariable) {
throw new Error(`Account-wide variable ${name} doesn't exist`);
}
let explicitSelect = false;
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)
);
}
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()}.`
);
}
}
}
private validateInputs(
flags: UnlinkFlags,
{ environment }: { environment?: string }
): UnlinkFlags {
if (flags['non-interactive']) {
if (!flags['variable-name']) {
throw new Error(
'Current name is required in non-interactive mode. Run the command with --variable-name flag.'
);
}
}
if (environment) {
environment = environment.toUpperCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return {
environment: [environment],
...flags,
};
}
return flags;
}
}
@@ -1,33 +1,106 @@
import { Platform } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import openBrowserAsync from 'better-opn';
import chalk from 'chalk';
import { getExpoWebsiteBaseUrl } from '../../api';
import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
import { GetServerSideEnvironmentVariablesFn } from '../../commandUtils/context/ServerSideEnvironmentVariablesContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { AppPlatform, BuildStatus } from '../../graphql/generated';
import {
EasEnvironmentFlagParameters,
EasNonInteractiveAndJsonFlags,
} from '../../commandUtils/flags';
import { diffFingerprint } from '../../fingerprint/cli';
import { abridgedDiff } from '../../fingerprint/diff';
import { Fingerprint, FingerprintDiffItem } from '../../fingerprint/types';
import {
getFingerprintInfoFromLocalProjectForPlatformsAsync,
stringToAppPlatform,
} from '../../fingerprint/utils';
import {
AppPlatform,
BuildFragment,
BuildStatus,
EnvironmentVariableEnvironment,
FingerprintFragment,
UpdateFragment,
} from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { FingerprintQuery } from '../../graphql/queries/FingerprintQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { learnMore } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { selectAsync } from '../../prompts';
import { Fingerprint, FingerprintDiffItem } from '../../utils/fingerprint';
import { createFingerprintAsync, diffFingerprint } from '../../utils/fingerprintCli';
import { abridgedDiff } from '../../utils/fingerprintDiff';
import { promptAsync, selectAsync } from '../../prompts';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
import { enableJsonOutput } from '../../utils/json';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { Client } from '../../vcs/vcs';
enum FingerprintOriginType {
Build = 'build',
Update = 'update',
Hash = 'hash',
Project = 'project',
}
type FingerprintOrigin = {
type: FingerprintOriginType;
build?: BuildFragment;
update?: UpdateFragment;
};
export default class FingerprintCompare extends EasCommand {
static override description = 'compare fingerprints of the current project, builds and updates';
static override hidden = true;
static override description = 'compare fingerprints of the current project, builds, and updates';
static override strict = false;
static override examples = [
'$ eas fingerprint:compare \t # Compare fingerprints in interactive mode',
'$ eas fingerprint:compare <FINGERPRINT-HASH> \t # Compare fingerprint against local directory',
'$ eas fingerprint:compare <FINGERPRINT-HASH-1> <FINGERPRINT-HASH-2> \t # Compare provided fingerprints',
'$ eas fingerprint:compare --build-id <BUILD-ID> \t # Compare fingerprint from build against local directory',
'$ eas fingerprint:compare --build-id <BUILD-ID> --environment production \t # Compare fingerprint from build against local directory with the "production" environment',
'$ eas fingerprint:compare --build-id <BUILD-ID-1> --build-id <BUILD-ID-2>\t # Compare fingerprint from a build against another build',
'$ eas fingerprint:compare --build-id <BUILD-ID> --update-id <UPDATE-ID>\t # Compare fingerprint from build against fingerprint from update',
'$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> \t # Compare fingerprint from update against provided fingerprint',
];
static override args = [
{
name: 'hash1',
description:
"If provided alone, HASH1 is compared against the current project's fingerprint.",
required: false,
},
{
name: 'hash2',
description: 'If two hashes are provided, HASH1 is compared against HASH2.',
required: false,
},
];
static override flags = {
'build-id': Flags.string({
aliases: ['buildId'],
description: 'Compare the fingerprint with the build with the specified ID',
multiple: true,
}),
'update-id': Flags.string({
aliases: ['updateId'],
description: 'Compare the fingerprint with the update with the specified ID',
multiple: true,
}),
open: Flags.boolean({
description: 'Open the fingerprint comparison in the browser',
}),
environment: Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description:
'If generating a fingerprint from the local directory, use the specified environment.',
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -37,71 +110,95 @@ export default class FingerprintCompare extends EasCommand {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
...this.ContextOptions.ServerSideEnvironmentVariables,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(FingerprintCompare);
const { json: jsonFlag, 'non-interactive': nonInteractive, buildId: buildIdFromArg } = flags;
const { args, flags } = await this.parse(FingerprintCompare);
const { hash1, hash2 } = args;
const {
json,
'non-interactive': nonInteractive,
'build-id': buildIds,
'update-id': updateIds,
open,
environment,
} = flags;
const [buildId1, buildId2] = buildIds ?? [];
const [updateId1, updateId2] = updateIds ?? [];
const {
projectId,
privateProjectConfig: { projectDir },
loggedIn: { graphqlClient },
vcsClient,
getServerSideEnvironmentVariablesAsync,
} = await this.getContextAsync(FingerprintCompare, {
nonInteractive,
withServerSideEnvironment: null,
withServerSideEnvironment: environment ?? null,
});
if (jsonFlag) {
if (json) {
enableJsonOutput();
}
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
let buildId: string | null = buildIdFromArg;
if (!buildId) {
if (nonInteractive) {
throw new Error('Build ID must be provided in non-interactive mode');
const firstFingerprintInfo = await getFingerprintInfoAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
{
nonInteractive,
buildId: buildId1,
updateId: updateId1,
hash: hash1,
}
);
const { fingerprint: firstFingerprint, origin: firstFingerprintOrigin } = firstFingerprintInfo;
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
return;
}
}
const isFirstFingerprintSpecifiedByFlagOrArg = hash1 || buildId1 || updateId1;
const isSecondFingerprintSpecifiedByFlagOrArg = hash2 || buildId2 || updateId2;
const secondFingerprintInfo = await getFingerprintInfoAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
{
nonInteractive,
buildId: buildId2,
updateId: updateId2,
hash: hash2,
useProjectFingerprint:
isFirstFingerprintSpecifiedByFlagOrArg && !isSecondFingerprintSpecifiedByFlagOrArg,
environmentForProjectFingerprint: environment,
},
firstFingerprintInfo
);
const { fingerprint: secondFingerprint, origin: secondFingerprintOrigin } =
secondFingerprintInfo;
Log.log(`Comparing fingerprints of the current project and build ${buildId}…`);
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
const fingerprintDebugUrl = buildWithFingerprint.fingerprint?.debugInfoUrl;
if (!fingerprintDebugUrl) {
Log.error('A fingerprint for the build could not be found.');
if (json) {
printJsonOnlyOutput({ fingerprint1: firstFingerprint, fingerprint2: secondFingerprint });
return;
}
const fingerprintResponse = await fetch(fingerprintDebugUrl);
const fingerprint = (await fingerprintResponse.json()) as Fingerprint;
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const buildPlatform = buildWithFingerprint.platform;
const workflow = workflows[appPlatformToPlatform(buildPlatform)];
const projectFingerprint = await createFingerprintAsync(projectDir, {
workflow,
platforms: [appPlatformToString(buildPlatform)],
debug: true,
env: undefined,
});
if (!projectFingerprint) {
Log.error('Project fingerprints can only be computed for projects with SDK 52 or higher');
return;
}
if (fingerprint.hash === projectFingerprint.hash) {
Log.log(`✅ Project fingerprint matches build`);
if (firstFingerprint.hash === secondFingerprint.hash) {
Log.log(
`✅ ${capitalizeFirstLetter(
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
)} matches ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
);
return;
} else {
Log.log(`🔄 Project fingerprint differs from build`);
Log.log(
`🔄 ${capitalizeFirstLetter(
prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin)
)} differs from ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}`
);
}
const fingerprintDiffs = diffFingerprint(projectDir, fingerprint, projectFingerprint);
const fingerprintDiffs = diffFingerprint(projectDir, firstFingerprint, secondFingerprint);
if (!fingerprintDiffs) {
Log.error('Fingerprint diffs can only be computed for projects with SDK 52 or higher');
return;
@@ -151,9 +248,390 @@ export default class FingerprintCompare extends EasCommand {
for (const diff of contentDiffs) {
printContentDiff(diff);
}
if (nonInteractive) {
return;
}
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
const fingerprintCompareUrl = new URL(
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/compare/${firstFingerprintInfo.fingerprint.hash}/${secondFingerprintInfo.fingerprint.hash}`,
getExpoWebsiteBaseUrl()
);
if (!open) {
Log.newLine();
Log.withInfo(
`💡 Use the --open flag to view the comparison in the browser. ${learnMore(
fingerprintCompareUrl.toString()
)}`
);
return;
}
await openBrowserAsync(fingerprintCompareUrl.toString());
}
}
async function getFingerprintInfoAsync(
graphqlClient: ExpoGraphqlClient,
projectDir: string,
projectId: string,
vcsClient: Client,
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn,
{
buildId,
updateId,
hash,
useProjectFingerprint,
environmentForProjectFingerprint,
nonInteractive,
}: {
buildId?: string;
updateId?: string;
hash: string;
useProjectFingerprint?: boolean;
environmentForProjectFingerprint?: string;
nonInteractive: boolean;
},
firstFingerprintInfo?: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
}
): Promise<{ fingerprint: Fingerprint; origin: FingerprintOrigin }> {
if (hash) {
return await getFingerprintInfoFromHashAsync(graphqlClient, projectId, hash);
} else if (updateId) {
return await getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
graphqlClient,
projectId,
nonInteractive,
updateId
);
} else if (buildId) {
return await getFingerprintInfoFromBuildIdAsync(graphqlClient, buildId);
} else if (useProjectFingerprint) {
if (!firstFingerprintInfo) {
throw new Error(
'First fingerprint must be provided in order to compare against the project.'
);
}
return await getFingerprintInfoFromLocalProjectAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environment: environmentForProjectFingerprint,
});
}
if (nonInteractive) {
throw new Error(
'Insufficent arguments provided for fingerprint comparison in non-interactive mode'
);
}
return await getFingerprintInfoInteractiveAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environmentForProjectFingerprint,
});
}
async function getFingerprintInfoInteractiveAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environmentForProjectFingerprint,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
vcsClient: Client;
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn;
firstFingerprintInfo?: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
};
environmentForProjectFingerprint?: string;
}): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const prompt = firstFingerprintInfo
? 'Select the second fingerprint to compare against'
: 'Select a reference fingerprint for comparison';
const originType = await selectAsync<FingerprintOriginType>(prompt, [
...(firstFingerprintInfo
? [{ title: 'Current project fingerprint', value: FingerprintOriginType.Project }]
: []),
{ title: 'Build fingerprint', value: FingerprintOriginType.Build },
{ title: 'Update fingerprint', value: FingerprintOriginType.Update },
{ title: 'Enter a fingerprint hash manually', value: FingerprintOriginType.Hash },
]);
if (originType === FingerprintOriginType.Project) {
if (!firstFingerprintInfo) {
throw new Error(
'First fingerprint must be provided in order to compare against the project.'
);
}
return await getFingerprintInfoFromLocalProjectAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environment: environmentForProjectFingerprint,
});
} else if (originType === FingerprintOriginType.Build) {
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
throw new Error('Must select build with fingerprint for comparison.');
}
return await getFingerprintInfoFromBuildIdAsync(graphqlClient, buildId);
} else if (originType === FingerprintOriginType.Update) {
const selectedBranch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'On which branch would you like search for an update?',
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
paginatedQueryOptions: {
json: false,
nonInteractive: false,
offset: 0,
},
});
const selectedUpdateGroup = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName: selectedBranch.name,
paginatedQueryOptions: {
json: false,
nonInteractive: false,
offset: 0,
},
});
const updateGroupId = selectedUpdateGroup[0].group;
return await getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
graphqlClient,
projectId,
false,
updateGroupId
);
} else if (originType === FingerprintOriginType.Hash) {
const { hash } = await promptAsync({
type: 'text',
name: 'hash',
message: 'Provide the fingerprint hash',
validate: (value: string) => !!value.trim(),
hint: '0000000000000000000000000000000000000000',
});
return await getFingerprintInfoFromHashAsync(graphqlClient, projectId, hash);
} else {
throw new Error(`Unsupported fingerprint origin type: ${originType}`);
}
}
async function getFingerprintInfoFromLocalProjectAsync({
graphqlClient,
projectDir,
projectId,
vcsClient,
getServerSideEnvironmentVariablesAsync,
firstFingerprintInfo,
environment,
}: {
graphqlClient: ExpoGraphqlClient;
projectDir: string;
projectId: string;
vcsClient: Client;
getServerSideEnvironmentVariablesAsync: GetServerSideEnvironmentVariablesFn;
firstFingerprintInfo: {
fingerprint: Fingerprint;
platforms?: AppPlatform[];
origin: FingerprintOrigin;
};
environment?: string;
}): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const firstFingerprintPlatforms = firstFingerprintInfo.platforms;
if (!firstFingerprintPlatforms || firstFingerprintPlatforms.length === 0) {
throw new Error(
`Cannot compare the local directory against the provided fingerprint hash "${firstFingerprintInfo.fingerprint.hash}" because the associated platform could not be determined. Ensure the fingerprint is linked to a build or update to identify the platform.`
);
}
if (environment) {
Log.log(`🔧 Using environment: ${environment}`);
}
const env = environment
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
: undefined;
const fingerprint = await getFingerprintInfoFromLocalProjectForPlatformsAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
firstFingerprintPlatforms,
{ env }
);
return { fingerprint, origin: { type: FingerprintOriginType.Project } };
}
async function getFingerprintFromUpdateFragmentAsync(
updateWithFingerprint: UpdateFragment
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
if (!updateWithFingerprint.fingerprint) {
throw new Error(`Fingerprint for update ${updateWithFingerprint.id} was not computed.`);
} else if (!updateWithFingerprint.fingerprint.debugInfoUrl) {
throw new Error(`Fingerprint source for update ${updateWithFingerprint.id} was not computed.`);
}
return {
fingerprint: await getFingerprintFromFingerprintFragmentAsync(
updateWithFingerprint.fingerprint
),
platforms: [stringToAppPlatform(updateWithFingerprint.platform)],
origin: {
type: FingerprintOriginType.Update,
update: updateWithFingerprint,
},
};
}
async function getFingerprintInfoFromHashAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
hash: string
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const fingerprintFragment = await getFingerprintFragmentFromHashAsync(
graphqlClient,
projectId,
hash
);
const fingerprint = await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment);
let platforms;
const fingerprintBuilds = fingerprintFragment.builds?.edges.map(edge => edge.node) ?? [];
const fingerprintUpdates = fingerprintFragment.updates?.edges.map(edge => edge.node) ?? [];
if (fingerprintBuilds.length > 0) {
platforms = [fingerprintBuilds[0].platform];
} else if (fingerprintUpdates.length > 0) {
platforms = [stringToAppPlatform(fingerprintUpdates[0].platform)];
}
return {
fingerprint,
platforms,
origin: {
type: FingerprintOriginType.Hash,
},
};
}
async function getFingerprintInfoFromUpdateGroupIdOrUpdateIdAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
nonInteractive: boolean,
updateGroupIdOrUpdateId: string
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
// Some people may pass in update group id instead of update id, so add interactive support for that
try {
const maybeUpdateGroupId = updateGroupIdOrUpdateId;
const updateGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: maybeUpdateGroupId,
});
if (updateGroup.length === 1) {
const update = updateGroup[0];
return await getFingerprintFromUpdateFragmentAsync(update);
}
if (nonInteractive) {
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
const updateUrl =
getExpoWebsiteBaseUrl() +
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/updates/${maybeUpdateGroupId}`;
throw new Error(
`Please pass in your update ID from ${updateUrl} or use interactive mode to select the update ID.`
);
}
const update = await selectAsync<UpdateFragment>(
'Select a platform to compute the fingerprint from',
updateGroup.map(update => ({
title: update.platform,
value: update,
}))
);
return await getFingerprintFromUpdateFragmentAsync(update);
} catch (error: any) {
if (!error?.message.includes('Could not find any updates with group ID')) {
throw error;
}
}
const updateId = updateGroupIdOrUpdateId;
const updateWithFingerprint = await UpdateQuery.viewByUpdateAsync(graphqlClient, {
updateId,
});
return await getFingerprintFromUpdateFragmentAsync(updateWithFingerprint);
}
async function getFingerprintInfoFromBuildIdAsync(
graphqlClient: ExpoGraphqlClient,
buildId: string
): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> {
const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId);
if (!buildWithFingerprint.fingerprint) {
throw new Error(`Fingerprint for build ${buildId} was not computed.`);
} else if (!buildWithFingerprint.fingerprint.debugInfoUrl) {
throw new Error(`Fingerprint source for build ${buildId} was not computed.`);
}
return {
fingerprint: await getFingerprintFromFingerprintFragmentAsync(buildWithFingerprint.fingerprint),
platforms: [buildWithFingerprint.platform],
origin: {
type: FingerprintOriginType.Build,
build: buildWithFingerprint,
},
};
}
async function getFingerprintFragmentFromHashAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
hash: string
): Promise<FingerprintFragment> {
const fingerprint = await FingerprintQuery.byHashAsync(graphqlClient, {
appId: projectId,
hash,
});
if (!fingerprint) {
const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
throw new Error(`Fingerprint with hash ${hash} was not uploaded for ${displayName}.`);
}
return fingerprint;
}
async function getFingerprintFromFingerprintFragmentAsync(
fingerprintFragment: Pick<FingerprintFragment, 'debugInfoUrl' | 'hash'>
): Promise<Fingerprint> {
const fingerprintDebugUrl = fingerprintFragment.debugInfoUrl;
if (!fingerprintDebugUrl) {
throw new Error(
`The source for fingerprint hash ${fingerprintFragment.hash} was not computed.`
);
}
const fingerprintResponse = await fetch(fingerprintDebugUrl);
return (await fingerprintResponse.json()) as Fingerprint;
}
function printContentDiff(diff: FingerprintDiffItem): void {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
@@ -259,7 +737,10 @@ const PRETTY_CONTENT_ID: Record<string, string> = {
'expoAutolinkingConfig:ios': 'Expo autolinking config (iOS)',
'expoAutolinkingConfig:android': 'Expo autolinking config (Android)',
'packageJson:scripts': 'package.json scripts',
expoConfig: 'Expo config',
expoConfig: 'Expo app config',
'package:react-native': 'React Native package.json',
'rncoreAutolinkingConfig:ios': 'React Native Community autolinking config (iOS)',
'rncoreAutolinkingConfig:android': 'React Native Community autolinking config (Android)',
};
function printContentSource({
@@ -303,6 +784,23 @@ function printContentsDiff(contents1: string | Buffer, contents2: string | Buffe
abridgedDiff(prettifiedContents1, prettifiedContents2, 0);
}
function prettyPrintFingerprint(fingerprint: Fingerprint, origin: FingerprintOrigin): string {
if (origin.type === FingerprintOriginType.Project) {
return `fingerprint ${fingerprint.hash} from local directory`;
} else if (origin.type === FingerprintOriginType.Update) {
return `fingerprint ${fingerprint.hash} from ${
origin.update?.platform ? stringToAppPlatform(origin.update?.platform) : ''
} ${origin.type}`;
} else if (origin.type === FingerprintOriginType.Build) {
return `fingerprint ${fingerprint.hash} from ${origin.build?.platform} ${origin.type}`;
}
return `fingerprint ${fingerprint.hash}`;
}
function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function isJSON(str: string): boolean {
try {
JSON.parse(str);
@@ -312,29 +810,7 @@ function isJSON(str: string): boolean {
}
}
function appPlatformToPlatform(platform: AppPlatform): Platform {
switch (platform) {
case AppPlatform.Android:
return Platform.ANDROID;
case AppPlatform.Ios:
return Platform.IOS;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
function appPlatformToString(platform: AppPlatform): string {
switch (platform) {
case AppPlatform.Android:
return 'android';
case AppPlatform.Ios:
return 'ios';
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
export async function selectBuildToCompareAsync(
async function selectBuildToCompareAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
projectDisplayName: string,
@@ -0,0 +1,164 @@
import { Env } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import { getExpoWebsiteBaseUrl } from '../../api';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EasEnvironmentFlagParameters,
EasNonInteractiveAndJsonFlags,
} from '../../commandUtils/flags';
import {
appPlatformToPlatform,
getFingerprintInfoFromLocalProjectForPlatformsAsync,
stringToAppPlatform,
} from '../../fingerprint/utils';
import { AppPlatform, EnvironmentVariableEnvironment } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { link } from '../../log';
import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getProfilesAsync } from '../../utils/profiles';
export default class FingerprintGenerate extends EasCommand {
static override description = 'generate fingerprints from the current project';
static override strict = false;
static override examples = [
'$ eas fingerprint:generate \t # Generate fingerprint in interactive mode',
'$ eas fingerprint:generate --build-profile preview \t # Generate a fingerprint using the "preview" build profile',
'$ eas fingerprint:generate --environment preview \t # Generate a fingerprint using the "preview" environment',
'$ eas fingerprint:generate --json --non-interactive --platform android \t # Output fingerprint json to stdout',
];
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
}),
...EASEnvironmentFlag,
environment: Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
exclusive: ['build-profile'],
}),
'build-profile': Flags.string({
char: 'e',
description: 'Name of the build profile from eas.json.',
exclusive: ['environment'],
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
...this.ContextOptions.ServerSideEnvironmentVariables,
...this.ContextOptions.DynamicProjectConfig,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(FingerprintGenerate);
const {
json,
'non-interactive': nonInteractive,
platform: platformStringFlag,
environment,
'build-profile': buildProfileName,
} = flags;
const {
projectId,
privateProjectConfig: { projectDir },
loggedIn: { graphqlClient },
vcsClient,
getServerSideEnvironmentVariablesAsync,
getDynamicPrivateProjectConfigAsync,
} = await this.getContextAsync(FingerprintGenerate, {
nonInteractive,
withServerSideEnvironment: environment ?? null,
});
if (json) {
enableJsonOutput();
}
let platform: AppPlatform;
if (platformStringFlag) {
platform = stringToAppPlatform(platformStringFlag);
} else {
if (nonInteractive) {
throw new Error('Platform must be specified in non-interactive mode with the --p flag');
}
platform = await selectRequestedPlatformAsync();
}
let env: Env | undefined = undefined;
if (environment) {
Log.log(`🔧 Using environment: ${environment}`);
env = { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' };
} else if (buildProfileName) {
Log.log(`🔧 Using build profile: ${buildProfileName}`);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const buildProfile = (
await getProfilesAsync({
type: 'build',
easJsonAccessor,
platforms: [appPlatformToPlatform(platform)],
profileName: buildProfileName ?? undefined,
projectDir,
})
)[0];
if (!buildProfile) {
throw new Error(`Build profile ${buildProfile} not found for platform: ${platform}`);
}
const configResult = await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});
env = configResult.env;
}
const fingerprint = await getFingerprintInfoFromLocalProjectForPlatformsAsync(
graphqlClient,
projectDir,
projectId,
vcsClient,
[platform],
{ env }
);
if (json) {
printJsonOnlyOutput(fingerprint);
return;
}
Log.log(`✅ Fingerprint generated: ${fingerprint.hash}`);
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
const fingerprintUrl = new URL(
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprint.hash}`,
getExpoWebsiteBaseUrl()
);
Log.log(`🔍 View the fingerprint at ${link(fingerprintUrl.toString())}`);
Log.log(`💡 If you want to see the entire fingerprint output, pass in the --json flag.`);
}
}
async function selectRequestedPlatformAsync(): Promise<AppPlatform> {
const { requestedPlatform } = await promptAsync({
type: 'select',
message: 'Select platform',
name: 'requestedPlatform',
choices: [
{ title: 'Android', value: AppPlatform.Android },
{ title: 'iOS', value: AppPlatform.Ios },
],
});
return requestedPlatform;
}
@@ -162,7 +162,10 @@ export default class Onboarding extends EasCommand {
cloneMethod,
});
const vcsClient = new GitClient(finalTargetProjectDirectory);
const vcsClient = new GitClient({
maybeCwdOverride: finalTargetProjectDirectory,
requireCommit: false,
});
if (!app.githubRepository) {
await fs.remove(path.join(finalTargetProjectDirectory, '.git'));
await runCommandAsync({
@@ -285,12 +288,12 @@ export default class Onboarding extends EasCommand {
} else {
Log.log('🚀 Now we are going to trigger your first build');
Log.log();
const { buildIds } = await runBuildAndSubmitAsync(
const { buildIds } = await runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
finalTargetProjectDirectory,
{
projectDir: finalTargetProjectDirectory,
flags: {
nonInteractive: true,
requestedPlatform:
platform === Platform.ANDROID ? RequestedPlatform.Android : RequestedPlatform.Ios,
@@ -304,11 +307,11 @@ export default class Onboarding extends EasCommand {
autoSubmit: false,
localBuildOptions: {},
freezeCredentials: false,
repack: true,
},
actor,
getDynamicProjectConfigFn
);
// eslint-disable-next-line async-protect/async-suffix
getDynamicPrivateProjectConfigAsync: getDynamicProjectConfigFn,
});
const buildId = buildIds[0];
Log.log();
Log.log('🚀 You can now go back to the website to continue:');
@@ -397,7 +400,7 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({
const bundleIdentifier = `com.${userPrefix}${stripInvalidCharactersForBundleIdentifier(
app.ownerAccount.name
)}.${slugPrefix}${stripInvalidCharactersForBundleIdentifier(app.slug)}`;
const updateUrl = getEASUpdateURL(app.id);
const updateUrl = getEASUpdateURL(app.id, /* manifestHostOverride */ null);
const easBuildGitHubConfig = {
android: {
+19
View File
@@ -26,6 +26,7 @@ import { maybeWarnAboutEasOutagesAsync } from '../utils/statuspageService';
interface RawCommandFlags {
platform?: string;
profile?: string;
'what-to-test'?: string;
latest?: boolean;
id?: string;
path?: string;
@@ -34,16 +35,19 @@ interface RawCommandFlags {
wait: boolean;
'non-interactive': boolean;
'verbose-fastlane': boolean;
groups?: string[];
}
interface CommandFlags {
requestedPlatform: RequestedPlatform;
profile?: string;
whatToTest?: string;
archiveFlags: SubmitArchiveFlags;
verbose: boolean;
wait: boolean;
nonInteractive: boolean;
isVerboseFastlaneEnabled: boolean;
groups?: string[];
}
export default class Submit extends EasCommand {
@@ -76,6 +80,9 @@ export default class Submit extends EasCommand {
description: 'App archive url',
exclusive: ['latest', 'id', 'path'],
}),
'what-to-test': Flags.string({
description: `Sets the "What to test" information in TestFlight (iOS only).`,
}),
verbose: Flags.boolean({
description: 'Always print logs from EAS Submit',
default: false,
@@ -89,6 +96,12 @@ export default class Submit extends EasCommand {
default: false,
description: 'Enable verbose logging for the submission process',
}),
groups: Flags.string({
description:
'Internal TestFlight testing groups to add the build to (iOS only). Learn more: https://developer.apple.com/help/app-store-connect/test-a-beta-version/add-internal-testers',
multiple: true,
char: 'g',
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
@@ -140,12 +153,14 @@ export default class Submit extends EasCommand {
archiveFlags: flagsWithPlatform.archiveFlags,
nonInteractive: flagsWithPlatform.nonInteractive,
isVerboseFastlaneEnabled: flagsWithPlatform.isVerboseFastlaneEnabled,
groups: flagsWithPlatform.groups,
actor,
graphqlClient,
analytics,
exp,
projectId,
vcsClient,
whatToTest: flagsWithPlatform.whatToTest,
specifiedProfile: flagsWithPlatform.profile,
});
@@ -184,6 +199,8 @@ export default class Submit extends EasCommand {
profile,
'non-interactive': nonInteractive,
'verbose-fastlane': isVerboseFastlaneEnabled,
groups,
'what-to-test': whatToTest,
...archiveFlags
} = flags;
@@ -204,7 +221,9 @@ export default class Submit extends EasCommand {
wait,
profile,
nonInteractive,
whatToTest,
isVerboseFastlaneEnabled,
groups,
};
}
@@ -18,8 +18,7 @@ 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';
import NoVcsClient from '../../vcs/clients/noVcs';
import GitClient from '../../vcs/clients/git';
/**
* This command will be run on the EAS workers.
@@ -65,10 +64,16 @@ export default class SubmitInternal extends EasCommand {
vcsClient,
} = await this.getContextAsync(SubmitInternal, {
nonInteractive: true,
vcsClientOverride: process.env.EAS_NO_VCS ? new NoVcsClient() : new GitNoCommitClient(),
withServerSideEnvironment: null,
});
if (vcsClient instanceof GitClient) {
// `build:internal` is run on EAS workers and the repo may have been changed
// by pre-install hooks or other scripts. We don't want to require committing changes
// to continue the build.
vcsClient.requireCommit = false;
}
const submissionProfile = await EasJsonUtils.getSubmitProfileAsync(
EasJsonAccessor.fromProjectPath(projectDir),
flags.platform,
@@ -91,6 +96,7 @@ export default class SubmitInternal extends EasCommand {
projectId,
vcsClient,
specifiedProfile: flags.profile,
groups: undefined, // use groups from submit profile
});
let config;
@@ -109,6 +115,7 @@ export default class SubmitInternal extends EasCommand {
const configInput: z.input<typeof SubmissionConfig.Ios.SchemaZ> = {
ascAppIdentifier: iosConfig.ascAppIdentifier,
isVerboseFastlaneEnabled: iosConfig.isVerboseFastlaneEnabled ?? undefined,
groups: iosConfig.groups ?? undefined,
...(ascApiKeyJson
? { ascApiJsonKey: ascApiKeyJson }
: {
@@ -35,6 +35,7 @@ const updateStub: UpdateFragment = {
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
isGitWorkingTreeDirty: false,
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
@@ -32,6 +32,7 @@ const updateStub: UpdateFragment = {
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
isGitWorkingTreeDirty: false,
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff. Show more