Compare commits

..
Author SHA1 Message Date
Phil Pluckthun 4a8d76c02a Replace sourcemap in output correctly 2025-04-30 14:20:26 +01:00
Phil Pluckthun 69ab03d96e Update CHANGELOG 2025-04-30 14:12:02 +01:00
Phil Pluckthun 69ed426842 Add sourcemap normalisation to eas-cli worker upload
See: https://github.com/expo/eas-hosting/blob/bd72f7ef735d956b1ce2f85b3d818447f33fd6ab/workers/deployment-api/src/services/workerUpload.ts#L35-L56
This needs to vaguely match: https://github.com/cloudflare/workers-sdk/blob/508a1a31a039a5f4700efbc7535a165d79b22cb9/packages/wrangler/src/deployment-bundle/source-maps.ts#L27-L63
2025-04-30 14:00:52 +01: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
Kadi Kraman bc282b6262 [eas-cli] Add environment flag to deploy script (#2763)
* Ensure worker env flag is passed through

* Add changelog
2024-12-11 16:43:43 +00:00
Phil Pluckthun 17661d1cf0 feat(eas-cli): Add --dry-run flag to deploy command (#2761)
* Add dry-run option to deploy command

* Add `--dry-run` flag to deploy command

* Update CHANGELOG
2024-12-11 16:28:12 +00:00
Will Schurman 8d35745124 [eas-cli] Upgrade @expo/multipart-body-parser (#2751) 2024-12-10 10:10:25 -08:00
Will Schurman 3740ee4ef9 [eas-cli] Remove random branch name generation for --auto branch name non-vcs fallback (#2747) 2024-12-10 08:56:01 -08:00
Expo CI 751d3d1287 update CHANGELOG.md 2024-12-10 10:30:23 +00:00
157 changed files with 12535 additions and 1217 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: ['khamilowicz', '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
-13
View File
@@ -226,16 +226,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 }}
+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: 'radoslawkrzemien,sjchmiela,khamilowicz'
github_token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/github-script@v3
+324 -1
View File
@@ -8,10 +8,333 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add support for enabeling broadcast Push Notifications capability option, by setting `ios.usesBroadcastPushNotifications` to `true` in app config. ([#2748](https://github.com/expo/eas-cli/pull/2748) by [@szdziedzic](https://github.com/szdziedzic))
-- 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))
### 🐛 Bug fixes
- Add missing sourcemap normalization to EAS Hosting worker deployment uploads. ([#3004](https://github.com/expo/eas-cli/pull/3004) by [@kitten](https://github.com/kitten))
### 🧹 Chores
## [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
- Add support for enabeling broadcast Push Notifications capability option, by setting `ios.usesBroadcastPushNotifications` to `true` in app config. ([#2748](https://github.com/expo/eas-cli/pull/2748) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Add link to FYI page mentioning the workaround for Apple SMS 2FA issue. ([#2752](https://github.com/expo/eas-cli/pull/2752) by [@szdziedzic](https://github.com/szdziedzic))
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "14.1.0",
"version": "16.3.3",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
@@ -0,0 +1,3 @@
# eas-build-cache-provider
A build cache provider plugin for the Expo CLI
@@ -0,0 +1,58 @@
{
"name": "eas-build-cache-provider",
"description": "A build cache provider plugin for the Expo CLI",
"version": "0.1.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/config": "10.0.6",
"@babel/code-frame": "7.23.5",
"@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,41 @@
import { getPackageJson } from '@expo/config';
import { SpawnResult } from '@expo/spawn-async';
// import from '@expo/config';
type RunOptions = any;
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,147 @@
import { Platform } 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';
// import from '@expo/config';
type CalculateFingerprintHashProps = any;
type RemoteBuildCachePlugin = any;
type RunOptions = any;
async function resolveRemoteBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
runOptions,
}: {
projectRoot: string;
platform: Platform;
fingerprintHash: string;
runOptions: RunOptions;
}): 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 remote build cache, upgrade your eas-cli installation to latest.`
);
}
return null;
}
}
async function uploadEASRemoteBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
buildPath,
}: {
projectRoot: string;
runOptions: RunOptions;
platform: Platform;
fingerprintHash: string;
buildPath?: string;
}): 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 remote cache}`);
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 EASRemoteBuildCacheProvider: RemoteBuildCachePlugin = {
resolveRemoteBuildCache: resolveRemoteBuildCacheAsync,
uploadRemoteBuildCache: uploadEASRemoteBuildCacheAsync,
calculateFingerprintHash: calculateEASFingerprintHashAsync,
};
export default EASRemoteBuildCacheProvider;
@@ -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/**/*"]
}
+329 -76
View File
@@ -69,6 +69,8 @@ eas --help COMMAND
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:delete [BUILD_ID]`](#eas-builddelete-build_id)
* [`eas build:dev`](#eas-builddev)
* [`eas build:download`](#eas-builddownload)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:resign`](#eas-buildresign)
@@ -88,6 +90,9 @@ eas --help COMMAND
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas credentials:configure-build`](#eas-credentialsconfigure-build)
* [`eas deploy [options]`](#eas-deploy-2moptions22m)
* [`eas deploy:alias`](#eas-deployalias)
* [`eas deploy:promote`](#eas-deploypromote)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:delete`](#eas-devicedelete)
* [`eas device:list`](#eas-devicelist)
@@ -102,6 +107,8 @@ eas --help COMMAND
* [`eas env:pull [ENVIRONMENT]`](#eas-envpull-environment)
* [`eas env:push [ENVIRONMENT]`](#eas-envpush-environment)
* [`eas env:update [ENVIRONMENT]`](#eas-envupdate-environment)
* [`eas fingerprint:compare [HASH1] [HASH2]`](#eas-fingerprintcompare-hash1-hash2)
* [`eas fingerprint:generate`](#eas-fingerprintgenerate)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory)
@@ -131,6 +138,8 @@ eas --help COMMAND
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
* [`eas whoami`](#eas-whoami)
* [`eas worker:alias`](#eas-workeralias)
* [`eas deploy [options]`](#eas-deploy-2moptions22m-1)
* [`eas workflow:create [NAME]`](#eas-workflowcreate-name)
* [`eas workflow:run [FILE]`](#eas-workflowrun-file)
* [`eas workflow:validate PATH`](#eas-workflowvalidate-path)
@@ -153,7 +162,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -170,7 +179,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -187,7 +196,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -201,7 +210,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -253,7 +262,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -274,7 +283,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -294,7 +303,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -314,7 +323,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -337,7 +346,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -345,16 +354,16 @@ start a build
```
USAGE
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache]
[--auto-submit | --auto-submit-with-profile <value>] [-m <value>] [--build-logger-level
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--json --non-interactive]
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache] [-s |
--auto-submit-with-profile <value>] [-m <value>] [--build-logger-level trace|debug|info|warn|error|fatal]
[--freeze-credentials] [--json --non-interactive]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to
"production" if defined in eas.json.
-m, --message=<value> A short message describing the build
-p, --platform=(android|ios|all)
--auto-submit Submit on build complete using the submit profile with the
-s, --auto-submit Submit on build complete using the submit profile with the
same name as the build profile
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with
provided name
@@ -374,7 +383,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -393,7 +402,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -410,7 +419,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -429,7 +438,48 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
run dev client simulator/emulator build with matching fingerprint or create a new one
```
USAGE
$ eas build:dev [-p ios|android] [-e <value>]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. It must be a profile allowing to create
emulator/simulator internal distribution dev client builds. The "development-simulator"
build profile will be selected by default.
-p, --platform=(ios|android)
DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
download simulator/emulator builds for a given fingerprint hash
```
USAGE
$ eas build:download --fingerprint <value> [-p ios|android] [--dev-client] [--json --non-interactive]
FLAGS
-p, --platform=(ios|android)
--[no-]dev-client Filter only dev-client builds.
--fingerprint=<value> (required) Fingerprint hash of the build to download
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
download simulator/emulator builds for a given fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/download.ts)_
## `eas build:inspect`
@@ -464,7 +514,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -475,8 +525,8 @@ USAGE
$ eas build:list [-p android|ios|all] [--status
new|in-queue|in-progress|pending-cancel|errored|finished|canceled] [--distribution store|internal|simulator]
[--channel <value>] [--app-version <value>] [--app-build-version <value>] [--sdk-version <value>] [--runtime-version
<value>] [--app-identifier <value>] [-e <value>] [--git-commit-hash <value>] [--offset <value>] [--limit <value>]
[--json --non-interactive] [--simulator]
<value>] [--app-identifier <value>] [-e <value>] [--git-commit-hash <value>] [--fingerprint-hash <value>] [--offset
<value>] [--limit <value>] [--json --non-interactive] [--simulator]
FLAGS
-e, --build-profile=<value> Filter only builds created with the
@@ -491,6 +541,8 @@ FLAGS
--channel=<value>
--distribution=(store|internal|simulator) Filter only builds with the specified
distribution type
--fingerprint-hash=<value> Filter only builds with the specified
fingerprint hash
--git-commit-hash=<value> Filter only builds created with the
specified git commit hash
--json Enable JSON output, non-JSON messages
@@ -514,7 +566,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -543,7 +595,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -569,7 +621,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -619,7 +671,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -638,7 +690,7 @@ DESCRIPTION
update version of an app
```
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -657,7 +709,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -674,7 +726,7 @@ DESCRIPTION
view a build for your project
```
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -695,7 +747,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -717,7 +769,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -737,7 +789,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -759,7 +811,7 @@ DESCRIPTION
pause a channel to stop it from sending updates
```
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/channel/pause.ts)_
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -781,7 +833,7 @@ DESCRIPTION
resume a channel to start sending updates
```
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/channel/resume.ts)_
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -814,7 +866,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -837,7 +889,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -858,7 +910,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -875,7 +927,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -893,7 +945,83 @@ DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
deploy your Expo Router web build and API Routes
```
USAGE
$ eas deploy [options]
$ eas deploy --prod
FLAGS
--alias=name Custom alias to assign to the new deployment.
--dry-run Outputs a tarball of the new deployment instead of uploading it.
--environment=(development|preview|production) Environment variable's environment
--export-dir=dir [default: dist] Directory where the Expo project was exported.
--id=xyz123 Custom unique identifier for the new deployment.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--prod Create a new production deployment.
DESCRIPTION
deploy your Expo Router web build and API Routes
ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
Assign deployment aliases.
```
USAGE
$ eas deploy:alias [--prod] [--alias <value>] [--id <value>] [--json --non-interactive]
FLAGS
--alias=name Custom alias to assign to the existing deployment.
--id=xyz123 Unique identifier of an existing deployment.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--prod Promote an existing deployment to production.
DESCRIPTION
Assign deployment aliases.
ALIASES
$ eas worker:alias
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/deploy/alias.ts)_
## `eas deploy:promote`
Assign deployment aliases.
```
USAGE
$ eas deploy:promote [--prod] [--alias <value>] [--id <value>] [--json --non-interactive]
FLAGS
--alias=name Custom alias to assign to the existing deployment.
--id=xyz123 Unique identifier of an existing deployment.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--prod Promote an existing deployment to production.
DESCRIPTION
Assign deployment aliases.
ALIASES
$ eas worker:alias
$ eas deploy:promote
```
## `eas device:create`
@@ -907,7 +1035,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -927,7 +1055,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -948,7 +1076,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -969,7 +1097,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -983,7 +1111,7 @@ DESCRIPTION
view a device for your project
```
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -997,7 +1125,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1026,7 +1154,7 @@ DESCRIPTION
create an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/create.ts)_
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1050,7 +1178,7 @@ DESCRIPTION
delete an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/delete.ts)_
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1071,7 +1199,7 @@ DESCRIPTION
execute a command with environment variables from the selected environment
```
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/exec.ts)_
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1096,7 +1224,7 @@ DESCRIPTION
view an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/get.ts)_
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1121,7 +1249,7 @@ DESCRIPTION
list environment variables for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/list.ts)_
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1144,7 +1272,7 @@ DESCRIPTION
pull environment variables for the selected environment to .env file
```
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/pull.ts)_
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1165,7 +1293,7 @@ DESCRIPTION
push environment variables from .env file to the selected environment
```
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/push.ts)_
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1196,7 +1324,83 @@ DESCRIPTION
update an environment variable on the current project or account
```
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/env/update.ts)_
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
compare fingerprints of the current project, builds, and updates
```
USAGE
$ eas fingerprint:compare [HASH1] [HASH2] [--build-id <value>] [--update-id <value>] [--open] [--environment
development|preview|production] [--json --non-interactive]
ARGUMENTS
HASH1 If provided alone, HASH1 is compared against the current project's fingerprint.
HASH2 If two hashes are provided, HASH1 is compared against HASH2.
FLAGS
--build-id=<value>... Compare the fingerprint with the build with the specified ID
--environment=(development|preview|production) If generating a fingerprint from the local directory, use the
specified environment.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--open Open the fingerprint comparison in the browser
--update-id=<value>... Compare the fingerprint with the update with the specified ID
DESCRIPTION
compare fingerprints of the current project, builds, and updates
EXAMPLES
$ eas fingerprint:compare # Compare fingerprints in interactive mode
$ eas fingerprint:compare <FINGERPRINT-HASH> # Compare fingerprint against local directory
$ eas fingerprint:compare <FINGERPRINT-HASH-1> <FINGERPRINT-HASH-2> # Compare provided fingerprints
$ eas fingerprint:compare --build-id <BUILD-ID> # Compare fingerprint from build against local directory
$ eas fingerprint:compare --build-id <BUILD-ID> --environment production # Compare fingerprint from build against local directory with the "production" environment
$ eas fingerprint:compare --build-id <BUILD-ID-1> --build-id <BUILD-ID-2> # Compare fingerprint from a build against another build
$ eas fingerprint:compare --build-id <BUILD-ID> --update-id <UPDATE-ID> # Compare fingerprint from build against fingerprint from update
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
generate fingerprints from the current project
```
USAGE
$ eas fingerprint:generate [-p android|ios] [--environment development|preview|production | -e <value>] [--json
--non-interactive]
FLAGS
-e, --build-profile=<value> Name of the build profile from eas.json.
-p, --platform=(android|ios)
--environment=(development|preview|production) Environment variable's environment
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
generate fingerprints from the current project
EXAMPLES
$ eas fingerprint:generate # Generate fingerprint in interactive mode
$ eas fingerprint:generate --build-profile preview # Generate a fingerprint using the "preview" build profile
$ eas fingerprint:generate --environment preview # Generate a fingerprint using the "preview" environment
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
```
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/fingerprint/generate.ts)_
## `eas help [COMMAND]`
@@ -1304,7 +1508,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1321,7 +1525,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1338,7 +1542,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1368,7 +1572,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1382,7 +1586,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1405,7 +1609,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1423,7 +1627,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -1454,7 +1658,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1496,7 +1700,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1516,7 +1720,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1537,7 +1741,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -1561,7 +1765,7 @@ DESCRIPTION
edit all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/edit.ts)_
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:list`
@@ -1583,7 +1787,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1615,7 +1819,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1643,7 +1847,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1663,7 +1867,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1683,7 +1887,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1704,7 +1908,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1724,7 +1928,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1742,7 +1946,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1764,7 +1968,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1781,7 +1985,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -1798,6 +2002,55 @@ ALIASES
$ eas whoami
```
## `eas worker:alias`
Assign deployment aliases.
```
USAGE
$ eas worker:alias [--prod] [--alias <value>] [--id <value>] [--json --non-interactive]
FLAGS
--alias=name Custom alias to assign to the existing deployment.
--id=xyz123 Unique identifier of an existing deployment.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--prod Promote an existing deployment to production.
DESCRIPTION
Assign deployment aliases.
ALIASES
$ eas worker:alias
$ eas deploy:promote
```
## `eas deploy [options]`
deploy your Expo Router web build and API Routes
```
USAGE
$ eas deploy [options]
$ eas deploy --prod
FLAGS
--alias=name Custom alias to assign to the new deployment.
--dry-run Outputs a tarball of the new deployment instead of uploading it.
--environment=(development|preview|production) Environment variable's environment
--export-dir=dir [default: dist] Directory where the Expo project was exported.
--id=xyz123 Custom unique identifier for the new deployment.
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--prod Create a new production deployment.
DESCRIPTION
deploy your Expo Router web build and API Routes
ALIASES
$ eas worker:deploy
```
## `eas workflow:create [NAME]`
create a new workflow configuration YAML file
@@ -1816,7 +2069,7 @@ DESCRIPTION
create a new workflow configuration YAML file
```
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/workflow/create.ts)_
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:run [FILE]`
@@ -1836,7 +2089,7 @@ DESCRIPTION
Run an EAS workflow
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/workflow/run.ts)_
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:validate PATH`
@@ -1856,5 +2109,5 @@ DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v14.1.0/packages/eas-cli/src/commands/workflow/validate.ts)_
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/workflow/validate.ts)_
<!-- commandsstop -->
+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);
+4419 -106
View File
File diff suppressed because it is too large. Load diff
+11 -8
View File
@@ -1,25 +1,25 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "14.1.0",
"version": "16.3.3",
"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.3.3",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.117",
"@expo/multipart-body-parser": "1.1.0",
"@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",
@@ -68,6 +68,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 +79,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 +97,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",
+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 () => {
+2 -4
View File
@@ -1,9 +1,7 @@
import { Client } from '../vcs/vcs';
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
return (
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
);
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string | null> {
return await vcsClient.getBranchNameAsync();
}
export class BranchNotFoundError extends Error {
@@ -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 () => {});
@@ -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',
@@ -151,6 +151,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),
@@ -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,
@@ -73,11 +73,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 +96,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 && {
+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();
+11 -17
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([
@@ -709,7 +700,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 +711,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}`,
+1
View File
@@ -63,5 +63,6 @@ export interface BuildContext<T extends Platform> {
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
isVerboseLoggingEnabled: boolean;
env: Record<string, string>;
}
@@ -43,6 +43,7 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel,
freezeCredentials,
repack,
isVerboseLoggingEnabled,
env,
}: {
buildProfileName: string;
@@ -65,6 +66,7 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled: boolean;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
@@ -151,6 +153,7 @@ export async function createBuildContextAsync<T extends Platform>({
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
isVerboseLoggingEnabled,
env,
};
if (platform === 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,
+6 -1
View File
@@ -65,13 +65,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 +92,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: {
+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 {
/**
-1
View File
@@ -62,7 +62,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);
+60 -26
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 {
@@ -97,18 +98,32 @@ export interface BuildFlags {
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled?: boolean;
}
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,
@@ -308,7 +325,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 +350,7 @@ export async function runBuildAndSubmitAsync(
}
return {
buildIds: startedBuilds.map(({ build }) => build.id),
buildProfiles,
};
}
@@ -384,6 +402,7 @@ async function prepareAndStartBuildAsync({
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
env,
});
@@ -531,18 +550,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 +580,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);
}
@@ -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];
}
+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();
}
+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>>;
@@ -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(
@@ -103,6 +103,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.
@@ -113,6 +115,9 @@ export async function findProjectRootAsync({
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
const projectDir = await findProjectRootAsync();
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'
)}`
);
}
}
}
@@ -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();
@@ -58,7 +58,7 @@ export default class BranchCreate extends EasCommand {
type: 'text',
name: 'name',
message: 'Provide a branch name:',
initial: await getDefaultBranchNameAsync(vcsClient),
initial: (await getDefaultBranchNameAsync(vcsClient)) ?? undefined,
validate: value => (value ? true : validationMessage),
}));
}
+351
View File
@@ -0,0 +1,351 @@
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,
});
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,
});
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: {},
repack: false,
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,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: Platform;
fingerprint?: { hash: 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,
},
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;
}
+64 -5
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,6 +37,7 @@ interface RawBuildFlags {
message?: string;
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
'verbose-logs'?: boolean;
repack: boolean;
}
@@ -78,10 +80,12 @@ 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',
@@ -115,6 +119,10 @@ export default class Build extends EasCommand {
hidden: true,
description: 'Use the golden dev client build repack flow as it works for onboarding',
}),
'verbose-logs': Flags.boolean({
default: false,
description: 'Use verbose logs for the build process',
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -158,15 +166,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(
@@ -230,6 +243,7 @@ export default class Build extends EasCommand {
message,
buildLoggerLevel: flags['build-logger-level'],
freezeCredentials: flags['freeze-credentials'],
isVerboseLoggingEnabled: flags['verbose-logs'],
repack: flags.repack,
};
}
@@ -266,6 +280,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,
@@ -124,8 +124,8 @@ export default class BuildInspect extends EasCommand {
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,
@@ -91,7 +96,7 @@ export default class BuildInternal extends EasCommand {
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,
})
@@ -33,12 +33,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 +70,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,
@@ -1,14 +1,17 @@
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';
@@ -43,16 +46,18 @@ interface DeployFlags {
environment?: EnvironmentVariableEnvironment;
deploymentIdentifier?: string;
exportDir: string;
dryRun: boolean;
}
interface RawDeployFlags {
'non-interactive': boolean;
environment?: string;
environment?: EnvironmentVariableEnvironment;
json: boolean;
prod: boolean;
alias?: string;
id?: string;
'export-dir': string;
'dry-run': boolean;
}
interface DeployInProgressParams {
@@ -63,13 +68,10 @@ interface DeployInProgressParams {
}
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({
@@ -90,10 +92,11 @@ export default class WorkerDeploy extends EasCommand {
helpValue: 'dir',
default: 'dist',
}),
environment: {
...EASEnvironmentFlag.environment,
description: 'Deploy with EAS Environment Variables matching the specified environment.',
},
'dry-run': Flags.boolean({
description: 'Outputs a tarball of the new deployment instead of uploading it.',
default: false,
}),
...EASEnvironmentFlag,
...EasNonInteractiveAndJsonFlags,
};
@@ -111,7 +114,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,
@@ -120,7 +123,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);
@@ -250,7 +256,7 @@ export default class WorkerDeploy extends EasCommand {
let progress = ora('Preparing project').start();
try {
const manifest = await WorkerAssets.createManifestAsync(
const manifestResult = await WorkerAssets.createManifestAsync(
{
environment: flags.environment,
projectDir,
@@ -258,16 +264,33 @@ export default class WorkerDeploy extends EasCommand {
},
graphqlClient
);
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(' ')
);
}
assetMap = await WorkerAssets.createAssetMapAsync(
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path
);
tarPath = await WorkerAssets.packFilesIterableAsync(
emitWorkerTarballAsync({
assetMap,
manifest,
manifest: manifestResult.manifest,
})
);
if (flags.dryRun) {
const DRY_RUN_OUTPUT_PATH = 'deploy.tar.gz';
await fs.promises.copyFile(tarPath, DRY_RUN_OUTPUT_PATH);
progress.succeed('Saved deploy.tar.gz tarball');
if (flags.json) {
printJsonOnlyOutput({ tarPath: DRY_RUN_OUTPUT_PATH });
}
return;
}
const uploadUrl = await getSignedDeploymentUrlAsync(graphqlClient, {
appId: projectId,
deploymentIdentifier: flags.deploymentIdentifier,
@@ -282,6 +305,32 @@ 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;
}
@@ -382,6 +431,8 @@ export default class WorkerDeploy extends EasCommand {
aliasName: flags.alias?.trim().toLowerCase(),
deploymentIdentifier: flags.id?.trim(),
exportDir: flags['export-dir'],
environment: flags['environment'],
dryRun: flags['dry-run'],
};
}
}
@@ -411,7 +462,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"`
);
}
@@ -436,7 +487,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;
+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) {
@@ -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,
@@ -307,8 +310,9 @@ export default class Onboarding extends EasCommand {
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:');
@@ -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,
@@ -54,6 +54,7 @@ export default class UpdateConfigure extends EasCommand {
platform: flags['platform'],
vcsClient,
env: undefined,
forceNativeConfigSync: true,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
+18 -10
View File
@@ -6,37 +6,43 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
DeleteUpdateGroupMutation,
UpdateMutationDeleteUpdateGroupArgs,
BackgroundJobReceiptDataFragment,
ScheduleUpdateGroupDeletionMutation,
ScheduleUpdateGroupDeletionMutationVariables,
} from '../../graphql/generated';
import { BackgroundJobReceiptNode } from '../../graphql/types/BackgroundJobReceipt';
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
async function deleteUpdateGroupAsync(
async function scheduleUpdateGroupDeletionAsync(
graphqlClient: ExpoGraphqlClient,
{
group,
}: {
group: string;
}
): Promise<DeleteUpdateGroupMutation> {
return await withErrorHandlingAsync(
): Promise<BackgroundJobReceiptDataFragment> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteUpdateGroupMutation, UpdateMutationDeleteUpdateGroupArgs>(
.mutation<ScheduleUpdateGroupDeletionMutation, ScheduleUpdateGroupDeletionMutationVariables>(
gql`
mutation DeleteUpdateGroup($group: ID!) {
mutation ScheduleUpdateGroupDeletion($group: ID!) {
update {
deleteUpdateGroup(group: $group) {
group
scheduleUpdateGroupDeletion(group: $group) {
id
...BackgroundJobReceiptData
}
}
}
${BackgroundJobReceiptNode}
`,
{ group }
)
.toPromise()
);
return result.update.scheduleUpdateGroupDeletion;
}
export default class UpdateDelete extends EasCommand {
@@ -90,7 +96,9 @@ export default class UpdateDelete extends EasCommand {
}
}
await deleteUpdateGroupAsync(graphqlClient, { group });
const receipt = await scheduleUpdateGroupDeletionAsync(graphqlClient, { group });
const successfulReceipt = await pollForBackgroundJobReceiptAsync(graphqlClient, receipt);
Log.debug('Deletion result', { successfulReceipt });
if (jsonFlag) {
printJsonOnlyOutput({ group });
+152 -91
View File
@@ -3,6 +3,7 @@ import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { getExpoWebsiteBaseUrl } from '../../api';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { transformFingerprintSource } from '../../build/graphql';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
@@ -12,8 +13,9 @@ import { EasNonInteractiveAndJsonFlags, EasUpdateEnvironmentFlag } from '../../c
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
AssetMapSourceInput,
EnvironmentVariableEnvironment,
FingerprintInfoGroup,
FingerprintInfoGroup as GraphqlFingerprintInfoGroup,
PublishUpdateGroupInput,
StatuspageServiceName,
UpdateInfoGroup,
@@ -21,9 +23,11 @@ import {
UpdateRolloutInfoGroup,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { maybeUploadAssetMapAsync } from '../../project/maybeUploadAssetMapAsync';
import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprintAsync';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
@@ -33,6 +37,7 @@ import {
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
filterCollectedAssetsByRequestedPlatforms,
findCompatibleBuildsAsync,
generateEasMetadataAsync,
getBranchNameForCommandAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
@@ -237,6 +242,10 @@ export default class UpdatePublish extends EasCommand {
jsonFlag,
});
const maybeServerEnv = environment
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
: {};
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
@@ -247,11 +256,7 @@ export default class UpdatePublish extends EasCommand {
exp,
platformFlag: requestedPlatform,
clearCache,
extraEnv: {
...(environment
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
: {}),
},
extraEnv: maybeServerEnv,
});
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
@@ -268,9 +273,14 @@ export default class UpdatePublish extends EasCommand {
let uploadedAssetCount = 0;
let assetLimitPerUpdateGroup = 0;
let realizedPlatforms: UpdatePublishPlatform[] = [];
let assetMapSource: AssetMapSourceInput | null = null;
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const [collectedAssets, maybeAssetMapSource] = await Promise.all([
collectAssetsAsync(distRoot),
maybeUploadAssetMapAsync(distRoot, graphqlClient),
]);
assetMapSource = maybeAssetMapSource;
const assets = filterCollectedAssetsByRequestedPlatforms(collectedAssets, requestedPlatform);
realizedPlatforms = Object.keys(assets) as UpdatePublishPlatform[];
@@ -393,7 +403,7 @@ export default class UpdatePublish extends EasCommand {
...workflows,
web: Workflow.UNKNOWN,
},
env: undefined,
env: maybeServerEnv,
});
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
@@ -410,11 +420,11 @@ export default class UpdatePublish extends EasCommand {
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
return {
...info,
fingerprintSource: info.fingerprint
expoUpdatesRuntimeFingerprintSource: info.expoUpdatesRuntimeFingerprint
? (
await maybeUploadFingerprintAsync({
hash: info.runtimeVersion,
fingerprint: info.fingerprint,
fingerprint: info.expoUpdatesRuntimeFingerprint,
graphqlClient,
})
).fingerprintSource ?? null
@@ -449,7 +459,7 @@ export default class UpdatePublish extends EasCommand {
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(
({ runtimeVersion, platforms, fingerprintSource, fingerprintInfoGroup }) => {
({ runtimeVersion, platforms, fingerprintInfoGroup }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
@@ -478,22 +488,25 @@ export default class UpdatePublish extends EasCommand {
},
};
},
{} as FingerprintInfoGroup
{} as GraphqlFingerprintInfoGroup
);
const assetMapGroup = assetMapSource
? Object.fromEntries(platforms.map(platform => [platform, assetMapSource]))
: null;
return {
branchId: branch.id,
updateInfoGroup: localUpdateInfoGroup,
rolloutInfoGroup: localRolloutInfoGroup,
runtimeFingerprintSource: fingerprintSource
? transformFingerprintSource(fingerprintSource)
: null,
fingerprintInfoGroup: transformedFingerprintInfoGroup,
assetMapGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
environment: environment ?? null,
};
}
);
@@ -561,95 +574,143 @@ export default class UpdatePublish extends EasCommand {
}
if (jsonFlag) {
printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
} else {
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
return;
}
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
Log.addNewLineIfNone();
const runtimeToCompatibleBuilds = await Promise.all(
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
findCompatibleBuildsAsync(graphqlClient, projectId, obj)
)
);
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
...(newAndroidUpdate?.rolloutControlUpdate
? [
{
label: 'Android Rollout',
value: `${newAndroidUpdate.rolloutPercentage}% (Base update ID: ${newAndroidUpdate.rolloutControlUpdate.id})`,
},
]
: []),
...(newIosUpdate?.rolloutControlUpdate
? [
{
label: 'iOS Rollout',
value: `${newIosUpdate.rolloutPercentage}% (Base update ID: ${newIosUpdate.rolloutControlUpdate.id})`,
},
]
: []),
{ label: 'Message', value: updateMessage ?? '' },
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'EAS Dashboard', value: updateGroupLink },
])
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate ? [{ label: 'Android update ID', value: newAndroidUpdate.id }] : []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
...(newAndroidUpdate?.rolloutControlUpdate
? [
{
label: 'Android Rollout',
value: `${newAndroidUpdate.rolloutPercentage}% (Base update ID: ${newAndroidUpdate.rolloutControlUpdate.id})`,
},
]
: []),
...(newIosUpdate?.rolloutControlUpdate
? [
{
label: 'iOS Rollout',
value: `${newIosUpdate.rolloutPercentage}% (Base update ID: ${newIosUpdate.rolloutControlUpdate.id})`,
},
]
: []),
{ label: 'Message', value: updateMessage ?? '' },
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
if (isUploadedAssetCountAboveWarningThreshold(uploadedAssetCount, assetLimitPerUpdateGroup)) {
Log.warn(
`This update group contains ${uploadedAssetCount} assets and is nearing the server cap of ${assetLimitPerUpdateGroup}.\n` +
`${learnMore('https://docs.expo.dev/eas-update/optimize-assets/', {
learnMoreMessage: 'Consider optimizing your usage of assets',
dim: false,
})}.`
);
Log.addNewLineIfNone();
if (
isUploadedAssetCountAboveWarningThreshold(uploadedAssetCount, assetLimitPerUpdateGroup)
) {
Log.warn(
`This update group contains ${uploadedAssetCount} assets and is nearing the server cap of ${assetLimitPerUpdateGroup}.\n` +
`${learnMore('https://docs.expo.dev/eas-update/optimize-assets/', {
learnMoreMessage: 'Consider optimizing your usage of assets',
dim: false,
})}.`
);
Log.addNewLineIfNone();
}
const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
)?.fingerprintInfoGroupWithCompatibleBuilds;
if (fingerprintsWithoutCompatibleBuilds) {
const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
([_platform, fingerprintInfo]) => !fingerprintInfo.build
);
if (missingBuilds.length > 0) {
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
Log.warn('No compatible builds found for the following fingerprints:');
for (const [platform, fingerprintInfo] of missingBuilds) {
const fingerprintUrl = new URL(
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
getExpoWebsiteBaseUrl()
);
Log.warn(
formatFields(
[
{
label: `${this.prettyPlatform(platform)} fingerprint`,
value: fingerprintInfo.fingerprintHash,
},
{ label: 'URL', value: fingerprintUrl.toString() },
],
{
labelFormat: label => ` ${chalk.dim(label)}:`,
}
)
);
Log.addNewLineIfNone();
}
}
}
}
}
private prettyPlatform(updatePlatform: string): string {
switch (updatePlatform) {
case 'android':
return 'Android';
case 'ios':
return 'iOS';
default:
return updatePlatform;
}
}
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
@@ -208,8 +208,8 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
platform,
runtimeVersionInfo: {
runtimeVersion: selectedRuntime,
fingerprint: null,
fingerprintHash: null,
expoUpdatesRuntimeFingerprint: null,
expoUpdatesRuntimeFingerprintHash: null,
},
}))
);
+386
View File
@@ -0,0 +1,386 @@
import { IOSConfig } from '@expo/config-plugins';
import { Platform } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import fg from 'fast-glob';
import fs from 'fs-extra';
import StreamZip from 'node-stream-zip';
import path from 'path';
import tar from 'tar';
import { v4 as uuidv4 } from 'uuid';
import { getBuildLogsUrl } from '../build/utils/url';
import EasCommand from '../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
import {
DistributionType,
LocalBuildArchiveSourceType,
UploadSessionType,
} from '../graphql/generated';
import { FingerprintMutation } from '../graphql/mutations/FingerprintMutation';
import { LocalBuildMutation } from '../graphql/mutations/LocalBuildMutation';
import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import { promptAsync } from '../prompts';
import * as xcode from '../run/ios/xcode';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { fromNow } from '../utils/date';
import { enableJsonOutput, printJsonOnlyOutput } from '../utils/json';
import { getTmpDirectory } from '../utils/paths';
import { createProgressTracker } from '../utils/progress';
export default class BuildUpload extends EasCommand {
static override description = 'upload a local build and generate a sharable link';
static override hidden = true;
static override flags = {
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
char: 'p',
options: [Platform.IOS, Platform.ANDROID],
}),
'build-path': Flags.string({
description: 'Path for the local build',
}),
fingerprint: Flags.string({
description: 'Fingerprint hash of the local build',
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildUpload);
const {
'build-path': buildPath,
fingerprint: manualFingerprintHash,
json: jsonFlag,
'non-interactive': nonInteractive,
} = flags;
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(BuildUpload, {
nonInteractive,
});
if (jsonFlag) {
enableJsonOutput();
}
const platform = await this.selectPlatformAsync({ platform: flags.platform, nonInteractive });
const localBuildPath = await resolveLocalBuildPathAsync({
platform,
inputBuildPath: buildPath,
nonInteractive,
});
const {
fingerprintHash: buildFingerprintHash,
developmentClient,
simulator,
} = await extractAppMetadataAsync(localBuildPath, platform);
let fingerprint = manualFingerprintHash ?? buildFingerprintHash;
if (fingerprint) {
if (
manualFingerprintHash &&
buildFingerprintHash &&
manualFingerprintHash !== buildFingerprintHash &&
!nonInteractive
) {
const selectedAnswer = await promptAsync({
name: 'fingerprint',
message: `The provided fingerprint hash ${manualFingerprintHash} does not match the fingerprint hash of the build ${buildFingerprintHash}. Which fingerprint do you want to use?`,
type: 'select',
choices: [
{ title: manualFingerprintHash, value: manualFingerprintHash },
{ title: buildFingerprintHash, value: buildFingerprintHash },
],
});
fingerprint = String(selectedAnswer.fingerprint);
}
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
hash: fingerprint,
});
}
Log.log(`Using build ${localBuildPath}`);
Log.log(`Fingerprint hash: ${fingerprint ?? 'Unknown'}`);
Log.log('Uploading your app archive to EAS');
const bucketKey = await uploadAppArchiveAsync(graphqlClient, localBuildPath);
const build = await LocalBuildMutation.createLocalBuildAsync(
graphqlClient,
projectId,
{ platform: toAppPlatform(platform), simulator },
{ type: LocalBuildArchiveSourceType.Gcs, bucketKey },
{ distribution: DistributionType.Internal, fingerprintHash: fingerprint, developmentClient }
);
if (jsonFlag) {
printJsonOnlyOutput({ url: getBuildLogsUrl(build) });
return;
}
Log.withTick(`Shareable link to the build: ${getBuildLogsUrl(build)}`);
}
private async selectPlatformAsync({
nonInteractive,
platform,
}: {
nonInteractive: boolean;
platform?: Platform;
}): Promise<Platform> {
if (nonInteractive && !platform) {
throw new Error('Platform must be provided in non-interactive mode');
}
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;
}
}
async function resolveLocalBuildPathAsync({
platform,
inputBuildPath,
nonInteractive,
}: {
platform: Platform;
inputBuildPath?: string;
nonInteractive: boolean;
}): Promise<string> {
const rootDir = process.cwd();
let applicationArchivePatternOrPath: string[] = [];
if (inputBuildPath) {
applicationArchivePatternOrPath.push(inputBuildPath);
} else if (platform === Platform.ANDROID) {
applicationArchivePatternOrPath.push('android/app/build/outputs/**/*.{apk,aab}');
} else {
const xcworkspacePath = await xcode.resolveXcodeProjectAsync(rootDir);
const schemes = IOSConfig.BuildScheme.getRunnableSchemesFromXcodeproj(rootDir);
if (xcworkspacePath && schemes.length > 0) {
for (const scheme of schemes) {
const buildSettings = await xcode.getXcodeBuildSettingsAsync(xcworkspacePath, scheme.name);
applicationArchivePatternOrPath = applicationArchivePatternOrPath.concat(
buildSettings.map(({ buildSettings }) => `${buildSettings.BUILD_DIR}/**/*.app`)
);
}
}
}
let applicationArchives = await findArtifactsAsync({
rootDir,
patternOrPathArray: applicationArchivePatternOrPath,
});
if (applicationArchives.length === 0 && !nonInteractive && !inputBuildPath) {
Log.warn(`No application archives found at ${applicationArchivePatternOrPath}.`);
const { path } = await promptAsync({
type: 'text',
name: 'path',
message: 'Provide a path to the application archive:',
validate: value => (value ? true : 'Path may not be empty.'),
});
applicationArchives = await findArtifactsAsync({
rootDir,
patternOrPathArray: [path],
});
}
if (applicationArchives.length === 1) {
return applicationArchives[0];
}
if (applicationArchives.length > 1) {
// sort by modification time
const applicationArchivesInfo = await Promise.all(
applicationArchives.map(async archivePath => ({
path: archivePath,
stat: await fs.stat(archivePath),
}))
);
applicationArchivesInfo.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
if (nonInteractive) {
return applicationArchivesInfo[0].path;
}
const { selectedPath } = await promptAsync({
type: 'select',
name: 'selectedPath',
message: 'Found multiple application archives. Select one:',
choices: applicationArchivesInfo.map(archive => {
return {
title: `${
archive.path.startsWith(rootDir) ? path.relative(rootDir, archive.path) : archive.path
} (${fromNow(archive.stat.mtime)} ago)`,
value: archive.path,
};
}),
});
return selectedPath;
}
throw new Error(`Found no application archives at ${inputBuildPath}.`);
}
async function findArtifactsAsync({
rootDir,
patternOrPathArray,
}: {
rootDir: string;
patternOrPathArray: string[];
}): Promise<string[]> {
const files = new Set<string>();
for (const patternOrPath of patternOrPathArray) {
if (path.isAbsolute(patternOrPath) && (await fs.pathExists(patternOrPath))) {
files.add(patternOrPath);
} else {
const filesFound = await fg(patternOrPath, {
cwd: rootDir,
onlyFiles: false,
});
filesFound.forEach(file => files.add(file));
}
}
return [...files].map(filePath => {
// User may provide an absolute path as input in which case
// fg will return an absolute path.
if (path.isAbsolute(filePath)) {
return filePath;
}
// User may also provide a relative path in which case
// fg will return a path relative to rootDir.
return path.join(rootDir, filePath);
});
}
async function uploadAppArchiveAsync(
graphqlClient: ExpoGraphqlClient,
originalPath: string
): Promise<string> {
let filePath = originalPath;
if ((await fs.stat(filePath)).isDirectory()) {
await fs.mkdirp(getTmpDirectory());
const tarPath = path.join(getTmpDirectory(), `${uuidv4()}.tar.gz`);
const parentPath = path.dirname(originalPath);
const folderName = path.basename(originalPath);
await tar.create({ cwd: parentPath, file: tarPath, gzip: true }, [folderName]);
filePath = tarPath;
}
const fileSize = (await fs.stat(filePath)).size;
const bucketKey = await uploadFileAtPathToGCSAsync(
graphqlClient,
UploadSessionType.EasShareGcsAppArchive,
filePath,
createProgressTracker({
total: fileSize,
message: 'Uploading to EAS',
completedMessage: 'Uploaded to EAS',
})
);
return bucketKey;
}
type AppMetadata = {
fingerprintHash?: string;
developmentClient: boolean;
simulator: boolean;
};
async function extractAppMetadataAsync(
buildPath: string,
platform: Platform
): Promise<AppMetadata> {
let developmentClient = false;
let fingerprintHash: string | undefined;
const simulator = platform === Platform.IOS;
const basePath = platform === Platform.ANDROID ? 'assets/' : buildPath;
const fingerprintFilePath =
platform === Platform.ANDROID ? 'fingerprint' : 'EXUpdates.bundle/fingerprint';
const devMenuBundlePath =
platform === Platform.ANDROID ? 'EXDevMenuApp.android.js' : 'EXDevMenu.bundle/';
const buildExtension = path.extname(buildPath);
if (['.apk', '.aab'].includes(buildExtension)) {
const zip = new StreamZip.async({ file: buildPath });
try {
developmentClient = Boolean(await zip.entry(path.join(basePath, devMenuBundlePath)));
if (await zip.entry(path.join(basePath, fingerprintFilePath))) {
fingerprintHash = (await zip.entryData(path.join(basePath, fingerprintFilePath))).toString(
'utf-8'
);
}
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
} finally {
await zip.close();
}
} else if (buildExtension === '.app') {
developmentClient = await fs.exists(path.join(basePath, devMenuBundlePath));
if (await fs.exists(path.join(basePath, fingerprintFilePath))) {
fingerprintHash = await fs.readFile(path.join(basePath, fingerprintFilePath), 'utf8');
}
} else {
// Use tar to list files in the archive
try {
let fingerprintHashPromise: Promise<string> | undefined;
await tar.list({
file: buildPath,
// eslint-disable-next-line async-protect/async-suffix
onentry: entry => {
if (entry.path.endsWith(devMenuBundlePath)) {
developmentClient = true;
}
if (entry.path.endsWith(fingerprintFilePath)) {
fingerprintHashPromise = new Promise<string>(async (resolve, reject) => {
try {
let content = '';
for await (const chunk of entry) {
content += chunk.toString('utf8');
}
resolve(content);
} catch (error) {
reject(error);
}
});
}
},
});
if (fingerprintHashPromise !== undefined) {
fingerprintHash = await fingerprintHashPromise;
}
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
}
}
return {
developmentClient,
fingerprintHash,
simulator,
};
}
+13 -4
View File
@@ -9,7 +9,7 @@ import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisi
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
import Log, { link } from '../../log';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { uploadAccountScopedEasJsonAsync } from '../../project/uploadAccountScopedEasJsonAsync';
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
import { WorkflowFile } from '../../utils/workflowFile';
@@ -81,6 +81,7 @@ export default class WorkflowRun extends EasCommand {
let projectArchiveBucketKey: string;
let easJsonBucketKey: string;
let packageJsonBucketKey: string;
try {
({ projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
@@ -88,10 +89,17 @@ export default class WorkflowRun extends EasCommand {
vcsClient,
accountId: account.id,
}));
({ easJsonBucketKey } = await uploadAccountScopedEasJsonAsync({
({ fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
graphqlClient,
accountId: account.id,
projectDir,
filePath: path.join(projectDir, 'eas.json'),
maxSizeBytes: 1024 * 1024,
}));
({ fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
graphqlClient,
accountId: account.id,
filePath: path.join(projectDir, 'package.json'),
maxSizeBytes: 1024 * 1024,
}));
} catch (err) {
Log.error('Failed to upload project sources.');
@@ -113,6 +121,7 @@ export default class WorkflowRun extends EasCommand {
type: WorkflowProjectSourceType.Gcs,
projectArchiveBucketKey,
easJsonBucketKey,
packageJsonBucketKey,
},
},
}
@@ -120,7 +129,7 @@ export default class WorkflowRun extends EasCommand {
Log.newLine();
Log.succeed(
`Workflow started successfully. ${link(
`Workflow run started successfully. See logs: ${link(
getWorkflowRunUrl(account.name, projectName, workflowRunId)
)}`
);
@@ -15,6 +15,7 @@ export interface AndroidCredentials {
interface Options {
app: AppLookupParams;
name?: string;
}
export default class AndroidCredentialsProvider {
@@ -37,7 +38,7 @@ export default class AndroidCredentialsProvider {
}
private async getRemoteAsync(): Promise<AndroidCredentials> {
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
const setupBuildCredentialsAction = new SetUpBuildCredentials(this.options);
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
return this.toAndroidCredentials(buildCredentials);
}
@@ -0,0 +1,168 @@
import { Session } from '@expo/apple-utils';
import nock from 'nock';
import { createAppAsync } from '../ensureAppExists';
const FIXTURE_SUCCESS = {
data: {
type: 'apps',
id: '6741087677',
attributes: {
name: 'expo (xxx)',
bundleId: 'com.bacon.jan27.x',
},
},
};
const FIXTURE_INVALID_NAME = {
errors: [
{
id: 'b3e7ca18-e4ce-4e55-83ce-8fff35dbaeca',
status: '409',
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.INVALID_CHARACTERS',
title: 'An attribute value has invalid characters.',
detail:
'App Name contains certain Unicode symbols, emoticons, diacritics, special characters, or private use characters that are not permitted.',
source: {
pointer: '/included/1/name',
},
},
],
};
const FIXTURE_ALREADY_USED_ON_ACCOUNT = {
errors: [
{
id: 'b91aefc5-0e94-48d9-8613-5b1a464a20f0',
status: '409',
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE.SAME_ACCOUNT',
title:
'The provided entity includes an attribute with a value that has already been used on this account.',
detail:
'The app name you entered is already being used for another app in your account. If you would like to use the name for this app you will need to submit an update to your other app to change the name, or remove it from App Store Connect.',
source: {
pointer: '/included/1/name',
},
},
],
};
const FIXTURE_ALREADY_USED_ON_ANOTHER_ACCOUNT = {
errors: [
{
id: '72b960f2-9e51-4f19-8d83-7cc08d42fec4',
status: '409',
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE.DIFFERENT_ACCOUNT',
title:
'The provided entity includes an attribute with a value that has already been used on a different account.',
detail:
'The App Name you entered is already being used. If you have trademark rights to this name and would like it released for your use, submit a claim.',
source: {
pointer: '/included/1/name',
},
},
],
};
const MOCK_CONTEXT = {
providerId: 1337,
teamId: 'test-team-id',
token: 'test-token',
};
beforeAll(async () => {
// Mock setup cookies API calls.
nock('https://appstoreconnect.apple.com')
.get(`/olympus/v1/session`)
.reply(200, {
provider: {
providerId: 1337,
publicProviderId: 'xxx-xxx-xxx-xxx-xxx',
name: 'Evan Bacon',
contentTypes: ['SOFTWARE'],
subType: 'INDIVIDUAL',
},
});
await Session.fetchCurrentSessionInfoAsync();
});
function getNameFromBody(body: any): any {
return body.included.find((item: any) => item.id === '${new-appInfoLocalization-id}')?.attributes
?.name;
}
it('asserts invalid name cases', async () => {
const scope = nock('https://appstoreconnect.apple.com')
.post(`/iris/v1/apps`, body => {
expect(getNameFromBody(body)).toBe('Expo 🚀');
return true;
})
.reply(409, FIXTURE_INVALID_NAME);
// Already used on same account
nock('https://appstoreconnect.apple.com')
.post(`/iris/v1/apps`, body => {
expect(getNameFromBody(body)).toBe('Expo -');
return true;
})
.reply(409, FIXTURE_ALREADY_USED_ON_ACCOUNT);
// Already used on different account
nock('https://appstoreconnect.apple.com')
.post(`/iris/v1/apps`, body => {
expect(getNameFromBody(body)).toMatch(/Expo - \([\w\d]+\)/);
return true;
})
.reply(409, FIXTURE_ALREADY_USED_ON_ANOTHER_ACCOUNT);
// Success
nock('https://appstoreconnect.apple.com')
.post(`/iris/v1/apps`, body => {
expect(getNameFromBody(body)).toMatch(/Expo - \([\w\d]+\)/);
return true;
})
.reply(200, FIXTURE_SUCCESS);
await createAppAsync(MOCK_CONTEXT, {
bundleId: 'com.bacon.jan27.x',
name: 'Expo 🚀',
companyName: 'expo',
});
expect(scope.isDone()).toBeTruthy();
});
it('works on first try', async () => {
nock('https://appstoreconnect.apple.com').post(`/iris/v1/apps`).reply(200, FIXTURE_SUCCESS);
await createAppAsync(MOCK_CONTEXT, {
bundleId: 'com.bacon.jan27.x',
name: 'Expo',
companyName: 'expo',
});
});
it('doubles up entropy', async () => {
nock('https://appstoreconnect.apple.com')
.post(`/iris/v1/apps`)
.reply(409, FIXTURE_ALREADY_USED_ON_ANOTHER_ACCOUNT);
nock('https://appstoreconnect.apple.com')
.post(`/iris/v1/apps`)
.reply(409, FIXTURE_ALREADY_USED_ON_ANOTHER_ACCOUNT);
nock('https://appstoreconnect.apple.com')
.post(`/iris/v1/apps`, body => {
expect(getNameFromBody(body)).toMatch(/Expo \([\w\d]+\) \([\w\d]+\)/);
return true;
})
.reply(200, FIXTURE_SUCCESS);
await createAppAsync(MOCK_CONTEXT, {
bundleId: 'com.bacon.jan27.x',
name: 'Expo',
companyName: 'expo',
});
});
@@ -1,11 +1,26 @@
import { JsonFileCache } from '@expo/apple-utils';
import * as fs from 'fs-extra';
import { vol } from 'memfs';
import { promptAsync } from '../../../../prompts';
import { AppleTeamType } from '../authenticateTypes';
import { resolveAppleTeamAsync, resolveAscApiKeyAsync } from '../resolveCredentials';
import * as Keychain from '../keychain';
import {
resolveAppleTeamAsync,
resolveAscApiKeyAsync,
resolveUserCredentialsAsync,
} from '../resolveCredentials';
jest.mock('../../../../prompts');
jest.mock('fs');
jest.mock('@expo/apple-utils', () => {
return {
getCacheAsync: jest.fn(),
usernameCachePath: jest.fn(),
...jest.requireActual('@expo/apple-utils'),
};
});
jest.mock('../keychain');
const testAscApiKey = {
keyP8: 'test-key-p8',
@@ -89,3 +104,139 @@ describe(resolveAppleTeamAsync, () => {
});
});
});
describe(resolveUserCredentialsAsync, () => {
let KeychainMock: any;
beforeAll(() => {
JsonFileCache.getCacheAsync = jest.fn().mockImplementation(async (filePath: string) => {
const content = await fs.readFile(filePath, 'utf-8');
return content ? JSON.parse(content) : null;
});
KeychainMock = jest.mocked(Keychain);
});
beforeEach(() => {
jest.clearAllMocks();
KeychainMock.EXPO_NO_KEYCHAIN = '';
});
it('uses credentials resolved from options', async () => {
const result = await resolveUserCredentialsAsync({
username: 'fakeUsername',
password: 'fakePassword',
});
expect(result).toMatchObject({
username: 'fakeUsername',
password: 'fakePassword',
});
});
it('uses credentials resolved from environment', async () => {
process.env.EXPO_APPLE_ID = 'fakeUsername';
process.env.EXPO_APPLE_PASSWORD = 'fakePassword';
const result = await resolveUserCredentialsAsync({});
expect(result).toMatchObject({
username: 'fakeUsername',
password: 'fakePassword',
});
});
it('uses credentials from prompt, does not update cache if they match', async () => {
vol.fromJSON({
'./.app-store/auth/username.json': `
{
"username": "fakeUsername"
}
`,
});
jest.mocked(promptAsync).mockResolvedValueOnce({ username: 'fakeUsername' });
JsonFileCache.usernameCachePath = jest.fn().mockReturnValue('./.app-store/auth/username.json');
const cacheAsyncSpy = jest.spyOn(JsonFileCache, 'cacheAsync');
const result = await resolveUserCredentialsAsync({});
expect(result).toMatchObject({
username: 'fakeUsername',
});
expect(cacheAsyncSpy).not.toHaveBeenCalled();
});
it("uses credentials from prompt, updates cache if they don't match", async () => {
vol.fromJSON({
'./.app-store/auth/username.json': `
{
"username": "fakeUsername"
}
`,
});
jest.mocked(promptAsync).mockResolvedValueOnce({ username: 'newFakeUsername' });
JsonFileCache.usernameCachePath = jest.fn().mockReturnValue('./.app-store/auth/username.json');
const cacheAsyncSpy = jest.spyOn(JsonFileCache, 'cacheAsync');
expect(JSON.parse(await fs.readFile('./.app-store/auth/username.json', 'utf-8'))).toMatchObject(
{ username: 'fakeUsername' }
);
const result = await resolveUserCredentialsAsync({});
expect(result).toMatchObject({
username: 'newFakeUsername',
});
expect(cacheAsyncSpy).toHaveBeenCalledWith('./.app-store/auth/username.json', {
username: 'newFakeUsername',
});
expect(JSON.parse(await fs.readFile('./.app-store/auth/username.json', 'utf-8'))).toMatchObject(
{ username: 'newFakeUsername' }
);
});
it("uses credentials from prompt, doesn't read or update cache if EXPO_NO_KEYCHAIN is set", async () => {
vol.fromJSON({
'./.app-store/auth/username.json': `
{
"username": "fakeUsername"
}
`,
});
jest.mocked(promptAsync).mockResolvedValueOnce({ username: 'newFakeUsername' });
JsonFileCache.usernameCachePath = jest.fn().mockReturnValue('./.app-store/auth/username.json');
KeychainMock.EXPO_NO_KEYCHAIN = 'true';
const getCacheAsyncSpy = jest.spyOn(JsonFileCache, 'getCacheAsync');
const cacheAsyncSpy = jest.spyOn(JsonFileCache, 'cacheAsync');
expect(JSON.parse(await fs.readFile('./.app-store/auth/username.json', 'utf-8'))).toMatchObject(
{ username: 'fakeUsername' }
);
const result = await resolveUserCredentialsAsync({});
expect(result).toMatchObject({
username: 'newFakeUsername',
});
expect(getCacheAsyncSpy).not.toHaveBeenCalled();
expect(cacheAsyncSpy).not.toHaveBeenCalled();
await expect(fs.access('./.app-store/auth/username.json')).rejects.toThrow();
});
it('clears credentials from prompt if malformed before using and updating cache', async () => {
vol.fromJSON({
'./.app-store/auth/username.json': `
{
"username": "fakeUsername"
}
`,
});
jest.mocked(promptAsync).mockResolvedValueOnce({ username: '\x17newFake\x01Username\x1F' });
JsonFileCache.usernameCachePath = jest.fn().mockReturnValue('./.app-store/auth/username.json');
const cacheAsyncSpy = jest.spyOn(JsonFileCache, 'cacheAsync');
expect(JSON.parse(await fs.readFile('./.app-store/auth/username.json', 'utf-8'))).toMatchObject(
{ username: 'fakeUsername' }
);
const result = await resolveUserCredentialsAsync({});
expect(result).toMatchObject({
username: 'newFakeUsername',
});
expect(cacheAsyncSpy).toHaveBeenCalledWith('./.app-store/auth/username.json', {
username: 'newFakeUsername',
});
expect(JSON.parse(await fs.readFile('./.app-store/auth/username.json', 'utf-8'))).toMatchObject(
{ username: 'newFakeUsername' }
);
});
});
@@ -1,6 +1,7 @@
import { App, BundleId } from '@expo/apple-utils';
import { App, BundleId, RequestContext } from '@expo/apple-utils';
import { JSONObject } from '@expo/json-file';
import chalk from 'chalk';
import { randomBytes } from 'node:crypto';
import { getRequestContext, isUserAuthCtx } from './authenticate';
import { AuthCtx, UserAuthCtx } from './authenticateTypes';
@@ -188,7 +189,7 @@ export async function ensureAppExistsAsync(
/**
* **Does not support App Store Connect API (CI).**
*/
app = await App.createAsync(context, {
app = await createAppAsync(context, {
bundleId: bundleIdentifier,
name,
primaryLocale: language,
@@ -215,3 +216,120 @@ export async function ensureAppExistsAsync(
);
return app;
}
function sanitizeName(name: string): string {
return (
name
// Replace emojis with a `-`
.replace(/[\p{Emoji}]/gu, '-')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
);
}
export async function createAppAsync(
context: RequestContext,
props: {
bundleId: string;
name: string;
primaryLocale?: string;
companyName?: string;
sku?: string;
},
retryCount = 0
): Promise<App> {
try {
/**
* **Does not support App Store Connect API (CI).**
*/
return await App.createAsync(context, props);
} catch (error) {
if (retryCount >= 5) {
throw error;
}
if (error instanceof Error) {
const handleDuplicateNameErrorAsync = async (): Promise<App> => {
const generatedName = props.name + ` (${randomBytes(3).toString('hex')})`;
Log.warn(
`App name "${props.name}" is already taken. Using generated name "${generatedName}" which can be changed later from https://appstoreconnect.apple.com.`
);
// Sanitize the name and try again.
return await createAppAsync(
context,
{
...props,
name: generatedName,
},
retryCount + 1
);
};
if (isAppleError(error)) {
// New error class that is thrown when the name is already taken but belongs to you.
if (
error.data.errors.some(
e =>
e.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE.SAME_ACCOUNT' ||
e.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE.DIFFERENT_ACCOUNT'
)
) {
return await handleDuplicateNameErrorAsync();
}
}
if ('code' in error && typeof error.code === 'string') {
if (
// Name is invalid
error.code === 'APP_CREATE_NAME_INVALID'
// UnexpectedAppleResponse: An attribute value has invalid characters. - App Name contains certain Unicode symbols, emoticons, diacritics, special characters, or private use characters that are not permitted.
// Name is taken
) {
const sanitizedName = sanitizeName(props.name);
if (sanitizedName === props.name) {
throw error;
}
Log.warn(
`App name "${props.name}" contains invalid characters. Using sanitized name "${sanitizedName}" which can be changed later from https://appstoreconnect.apple.com.`
);
// Sanitize the name and try again.
return await createAppAsync(
context,
{
...props,
name: sanitizedName,
},
retryCount + 1
);
}
if (
// UnexpectedAppleResponse: The provided entity includes an attribute with a value that has already been used on a different account. - The App Name you entered is already being used. If you have trademark rights to
// this name and would like it released for your use, submit a claim.
error.code === 'APP_CREATE_NAME_UNAVAILABLE'
) {
return await handleDuplicateNameErrorAsync();
}
}
}
throw error;
}
}
export function isAppleError(error: any): error is {
data: {
errors: {
id: string;
status: string;
/** 'ENTITY_ERROR.ATTRIBUTE.INVALID.INVALID_CHARACTERS' */
code: string;
/** 'An attribute value has invalid characters.' */
title: string;
/** 'App Name contains certain Unicode symbols, emoticons, diacritics, special characters, or private use characters that are not permitted.' */
detail: string;
}[];
};
} {
return 'data' in error && 'errors' in error.data && Array.isArray(error.data.errors);
}
@@ -0,0 +1,222 @@
import { App, BetaGroup, Session, User, UserRole } from '@expo/apple-utils';
import { isAppleError } from './ensureAppExists';
import Log from '../../../log';
import { ora } from '../../../ora';
import { confirmAsync } from '../../../prompts';
// The name of the internal TestFlight group, this should probably never change.
const AUTO_GROUP_NAME = 'Team (Expo)';
/**
* Ensure a TestFlight internal group with access to all builds exists for the app and has all admin users invited to it.
* This allows users to instantly access their builds from TestFlight after it finishes processing.
*/
export async function ensureTestFlightGroupExistsAsync(app: App): Promise<void> {
if (process.env.EAS_NO_AUTO_TESTFLIGHT_SETUP) {
Log.debug('EAS_NO_AUTO_TESTFLIGHT_SETUP is set, skipping TestFlight setup');
return;
}
const groups = await app.getBetaGroupsAsync({
query: {
includes: ['betaTesters'],
},
});
if (groups.length > 0) {
Log.debug(`Found ${groups.length} TestFlight groups`);
Log.debug('Skipping creating a new TestFlight group');
return;
}
const group = await ensureInternalGroupAsync({
app,
groups,
});
const users = await User.getAsync(app.context);
const admins = users.filter(user => user.attributes.roles?.includes(UserRole.ADMIN));
await addAllUsersToInternalGroupAsync(group, admins);
}
async function ensureInternalGroupAsync({
groups,
app,
}: {
groups: BetaGroup[];
app: App;
}): Promise<BetaGroup> {
let betaGroup = groups.find(group => group.attributes.name === AUTO_GROUP_NAME);
if (!betaGroup) {
const spinner = ora().start('Creating TestFlight group...');
try {
// Apple throw an error if you create the group too quickly after creating the app. We'll retry a few times.
await pollRetryAsync(
async () => {
betaGroup = await app.createBetaGroupAsync({
name: AUTO_GROUP_NAME,
isInternalGroup: true,
// Automatically add latest builds to the group without needing to run the command.
hasAccessToAllBuilds: true,
});
},
{
shouldRetry(error) {
if (isAppleError(error)) {
spinner.text = `TestFlight still preparing, retrying in 10 seconds...`;
return error.data.errors.some(
error => error.code === 'ENTITY_ERROR.RELATIONSHIP.INVALID'
);
}
return false;
},
}
);
spinner.succeed(`TestFlight group created: ${AUTO_GROUP_NAME}`);
} catch (error: any) {
spinner.fail('Failed to create TestFlight group...');
throw error;
}
}
if (!betaGroup) {
throw new Error('Failed to create internal TestFlight group');
}
// `hasAccessToAllBuilds` is a newer feature that allows the group to automatically have access to all builds. This cannot be patched so we need to recreate the group.
if (!betaGroup.attributes.hasAccessToAllBuilds) {
if (
await confirmAsync({
message: 'Regenerate internal TestFlight group to allow automatic access to all builds?',
})
) {
await BetaGroup.deleteAsync(app.context, { id: betaGroup.id });
return await ensureInternalGroupAsync({
app,
groups: await app.getBetaGroupsAsync({
query: {
includes: ['betaTesters'],
},
}),
});
}
}
return betaGroup;
}
async function addAllUsersToInternalGroupAsync(group: BetaGroup, users: User[]): Promise<void> {
let emails = users
.filter(user => user.attributes.email)
.map(user => ({
email: user.attributes.email!,
firstName: user.attributes.firstName ?? '',
lastName: user.attributes.lastName ?? '',
}));
const { betaTesters } = group.attributes;
const existingEmails = betaTesters?.map(tester => tester.attributes.email).filter(Boolean) ?? [];
// Filter out existing beta testers.
if (betaTesters) {
emails = emails.filter(
user => !existingEmails.find(existingEmail => existingEmail === user.email)
);
}
// No new users to add to the internal group.
if (!emails.length) {
// No need to log which users are here on subsequent runs as devs already know the drill at this point.
Log.debug(`All current admins are already added to the group: ${group.attributes.name}`);
return;
}
Log.debug(`Adding ${emails.length} users to internal group: ${group.attributes.name}`);
Log.debug(`Users: ${emails.map(user => user.email).join(', ')}`);
const data = await group.createBulkBetaTesterAssignmentsAsync(emails);
const success = data.attributes.betaTesters.every(tester => {
if (tester.assignmentResult === 'FAILED') {
if (tester.errors && Array.isArray(tester.errors) && tester.errors.length) {
if (
tester.errors.length === 1 &&
tester.errors[0].key === 'Halliday.tester.already.exists'
) {
return true;
}
for (const error of tester.errors) {
Log.error(
`Error adding user ${tester.email} to TestFlight group "${group.attributes.name}": ${error.key}`
);
}
}
return false;
}
if (tester.assignmentResult === 'NOT_QUALIFIED_FOR_INTERNAL_GROUP') {
return false;
}
return true;
});
if (!success) {
const groupUrl = await getTestFlightGroupUrlAsync(group);
Log.error(
`Unable to add all admins to TestFlight group "${
group.attributes.name
}". You can add them manually in App Store Connect. ${groupUrl ?? ''}`
);
} else {
Log.log(
`TestFlight access enabled for: ` +
data.attributes.betaTesters
.map(tester => tester.email)
.filter(Boolean)
.join(', ')
);
// TODO: When we have more TestFlight functionality, we can link to it from here.
}
}
async function getTestFlightGroupUrlAsync(group: BetaGroup): Promise<string | null> {
if (group.context.providerId) {
try {
const session = await Session.getSessionForProviderIdAsync(group.context.providerId);
return `https://appstoreconnect.apple.com/teams/${session.provider.publicProviderId}/apps/6741088859/testflight/groups/${group.id}`;
} catch (error) {
// Avoid crashing if we can't get the session.
Log.debug('Failed to get session for provider ID', error);
}
}
return null;
}
async function pollRetryAsync<T>(
fn: () => Promise<T>,
{
shouldRetry,
retries = 15,
// 25 seconds was the minium interval I calculated when measuring against 5 second intervals.
// Switching to 10 seconds to account for days where Apple APIs are faster.
interval = 10000,
}: { shouldRetry?: (error: Error) => boolean; retries?: number; interval?: number } = {}
): Promise<T> {
let lastError: Error | null = null;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error: any) {
if (shouldRetry && !shouldRetry(error)) {
throw error;
}
lastError = error;
await new Promise(resolve => setTimeout(resolve, interval));
}
}
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw lastError;
}
@@ -29,6 +29,7 @@ function resolveProfileType(
return resolveProfileTypeIos(profileClass, isEnterprise);
case ApplePlatform.TV_OS:
return resolveProfileTypeAppleTv(profileClass, isEnterprise);
case ApplePlatform.VISION_OS:
case ApplePlatform.MAC_OS:
throw new Error(`${applePlatform} profiles are not supported`);
}
@@ -208,13 +208,18 @@ async function promptUsernameAsync(): Promise<string> {
// the default value for quicker authentication.
const lastAppleId = await getCachedUsernameAsync();
const { username } = await promptAsync({
let { username } = await promptAsync({
type: 'text',
name: 'username',
message: `Apple ID:`,
validate: (val: string) => val !== '',
initial: lastAppleId ?? undefined,
});
// https://github.com/expo/eas-cli/issues/2254
// user got an invalid unprintable character in their username, which was saved to cache and used to prefill the prompt
// which made them accept the prefilled value not knowing it contains the character and fail to log in
// this replace makes sure no such characters are used or cached
username = username.replace(/[\x00-\x1F]/gi, '');
if (username && username !== lastAppleId) {
await cacheUsernameAsync(username);
@@ -48,7 +48,7 @@ async function collectDataAndRegisterDeviceAsync(
}
const { udid, deviceClass, name } = deviceData;
const spinner = ora(`Registering Apple device on Expo`).start();
const spinner = ora(`Registering Apple device on EAS`).start();
try {
await AppleDeviceMutation.createAppleDeviceAsync(
graphqlClient,
@@ -45,7 +45,7 @@ async function collectDataAndRegisterDeviceAsync(
): Promise<void> {
const { udid, deviceClass, name } = await collectDeviceDataAsync(appleTeam);
const spinner = ora(`Registering Apple device on Expo`).start();
const spinner = ora(`Registering Apple device on EAS`).start();
try {
await AppleDeviceMutation.createAppleDeviceAsync(
graphqlClient,
@@ -1,17 +1,18 @@
import { Env, Workflow } from '@expo/eas-build-job';
import { silent as silentResolveFrom } from 'resolve-from';
import mapMapAsync from './expodash/mapMapAsync';
import { Fingerprint, FingerprintDiffItem } from './fingerprint';
import { Fingerprint, FingerprintDiffItem } from './types';
import Log from '../log';
import { ora } from '../ora';
import mapMapAsync from '../utils/expodash/mapMapAsync';
export type FingerprintOptions = {
workflow: Workflow;
workflow?: Workflow;
platforms: string[];
debug?: boolean;
env: Env | undefined;
cwd?: string;
ignorePaths?: string[];
};
export function diffFingerprint(
@@ -85,17 +86,38 @@ async function createFingerprintWithoutLoggingAsync(
> {
const Fingerprint = require(fingerprintPath);
const fingerprintOptions: Record<string, any> = {};
const ignorePaths = [];
if (options.workflow === Workflow.MANAGED) {
ignorePaths.push('android/**/*');
ignorePaths.push('ios/**/*');
}
if (options.ignorePaths) {
ignorePaths.push(...options.ignorePaths);
}
if (ignorePaths.length > 0) {
fingerprintOptions.ignorePaths = ignorePaths;
}
if (options.platforms) {
fingerprintOptions.platforms = [...options.platforms];
}
if (options.workflow === Workflow.MANAGED) {
fingerprintOptions.ignorePaths = ['android/**/*', 'ios/**/*'];
}
if (options.debug) {
fingerprintOptions.debug = true;
}
// eslint-disable-next-line @typescript-eslint/return-await
return await Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions);
return await withTemporaryEnvAsync(options.env ?? {}, () =>
Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions)
);
}
async function withTemporaryEnvAsync(envVars: Env, fn: () => Promise<any>): Promise<any> {
const originalEnv = { ...process.env };
Object.assign(process.env, envVars);
try {
return await fn();
} finally {
process.env = originalEnv;
}
}
/**
@@ -52,8 +52,13 @@ export function abridgedDiff(str1: string, str2: string, contextLines: number =
const flushChunk = (): void => {
if (currentChunk.length > 0) {
const originalRange = `${startOriginal},${removedLines || 0}`;
const modifiedRange = `${startModified},${addedLines || 0}`;
const contextLines = currentChunkPriorContext.length + currentChunkAfterContext.length;
const originalRange = `${(startOriginal ?? 1) - currentChunkPriorContext.length},${
contextLines + removedLines
}`;
const modifiedRange = `${(startModified ?? 1) - currentChunkPriorContext.length},${
contextLines + addedLines
}`;
// `git diff` style header
output.push(chalk.cyan(`@@ -${originalRange} +${modifiedRange} @@`));
output.push(...currentChunkPriorContext);
+112
View File
@@ -0,0 +1,112 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { createFingerprintAsync } from './cli';
import { Fingerprint } from './types';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPlatform } from '../graphql/generated';
import { FingerprintMutation } from '../graphql/mutations/FingerprintMutation';
import Log from '../log';
import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAsync';
import { resolveWorkflowPerPlatformAsync } from '../project/workflow';
import { Client } from '../vcs/vcs';
export async function getFingerprintInfoFromLocalProjectForPlatformsAsync(
graphqlClient: ExpoGraphqlClient,
projectDir: string,
projectId: string,
vcsClient: Client,
platforms: AppPlatform[],
{ env }: { env?: Record<string, string> } = {}
): Promise<Fingerprint> {
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const optionsFromWorkflow = getFingerprintOptionsFromWorkflow(platforms, workflows);
const projectFingerprint = await createFingerprintAsync(projectDir, {
...optionsFromWorkflow,
platforms: platforms.map(appPlatformToString),
debug: true,
env,
});
if (!projectFingerprint) {
throw new Error('Project fingerprints can only be computed for projects with SDK 52 or higher');
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: projectFingerprint.hash,
fingerprint: {
fingerprintSources: projectFingerprint.sources,
isDebugFingerprintSource: Log.isDebug,
},
graphqlClient,
});
await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, {
hash: uploadedFingerprint.hash,
source: uploadedFingerprint.fingerprintSource,
});
return projectFingerprint;
}
function getFingerprintOptionsFromWorkflow(
platforms: AppPlatform[],
workflowsByPlatform: Record<Platform, Workflow>
): { workflow?: Workflow; ignorePaths?: string[] } {
if (platforms.length === 0) {
throw new Error('Could not determine platform from fingerprint sources');
}
// Single platform case
if (platforms.length === 1) {
const platform = platforms[0];
return { workflow: workflowsByPlatform[appPlatformToPlatform(platform)] };
}
// Multiple platforms case
const workflows = platforms.map(platform => workflowsByPlatform[appPlatformToPlatform(platform)]);
// If all workflows are the same, return the common workflow
const [firstWorkflow, ...restWorkflows] = workflows;
if (restWorkflows.every(workflow => workflow === firstWorkflow)) {
return { workflow: firstWorkflow };
}
// Generate ignorePaths for mixed workflows
const ignorePaths = platforms
.filter(platform => workflowsByPlatform[appPlatformToPlatform(platform)] === Workflow.MANAGED)
.map(platform => `${appPlatformToString(platform)}/**/*`);
return { ignorePaths };
}
export 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}`);
}
}
export 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 function stringToAppPlatform(platform: string): AppPlatform {
switch (platform) {
case 'android':
return AppPlatform.Android;
case 'ios':
return AppPlatform.Ios;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,39 @@
import { FingerprintSource } from '@expo/eas-build-job';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import { CreateFingeprintMutation, FingerprintFragment } from '../generated';
import { FingerprintFragmentNode } from '../types/Fingerprint';
export const FingerprintMutation = {
async createFingerprintAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
fingerprintData: { hash: string; source?: FingerprintSource }
): Promise<FingerprintFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateFingeprintMutation>(
gql`
mutation CreateFingeprintMutation(
$fingerprintData: CreateFingerprintInput!
$appId: ID!
) {
fingerprint {
createOrGetExistingFingerprint(fingerprintData: $fingerprintData, appId: $appId) {
id
...FingerprintFragment
}
}
}
${print(FingerprintFragmentNode)}
`,
{ appId, fingerprintData }
)
.toPromise()
);
return data.fingerprint.createOrGetExistingFingerprint;
},
};
@@ -0,0 +1,55 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
BuildFragment,
BuildMetadataInput,
CreateLocalBuildMutation,
LocalBuildArchiveSourceInput,
LocalBuildJobInput,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
export const LocalBuildMutation = {
async createLocalBuildAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
job: LocalBuildJobInput,
artifactSource: LocalBuildArchiveSourceInput,
metadata: BuildMetadataInput
): Promise<BuildFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateLocalBuildMutation>(
gql`
mutation createLocalBuildMutation(
$appId: ID!
$jobInput: LocalBuildJobInput!
$artifactSource: LocalBuildArchiveSourceInput!
$metadata: BuildMetadataInput
) {
build {
createLocalBuild(
appId: $appId
job: $jobInput
artifactSource: $artifactSource
metadata: $metadata
) {
build {
id
...BuildFragment
}
}
}
}
${print(BuildFragmentNode)}
`,
{ appId, jobInput: job, artifactSource, metadata }
)
.toPromise()
);
return data.build.createLocalBuild.build;
},
};
@@ -20,20 +20,22 @@ export interface SignedUrl {
export const UploadSessionMutation = {
async createUploadSessionAsync(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType
type: UploadSessionType,
filename?: string
): Promise<SignedUrl> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUploadSessionMutation, CreateUploadSessionMutationVariables>(
gql`
mutation CreateUploadSessionMutation($type: UploadSessionType!) {
mutation CreateUploadSessionMutation($type: UploadSessionType!, $filename: String) {
uploadSession {
createUploadSession(type: $type)
createUploadSession(type: $type, filename: $filename)
}
}
`,
{
type,
filename,
}
)
.toPromise()
@@ -0,0 +1,40 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
BackgroundJobReceiptByIdQuery,
BackgroundJobReceiptByIdQueryVariables,
} from '../generated';
import { BackgroundJobReceiptNode } from '../types/BackgroundJobReceipt';
export const BackgroundJobReceiptQuery = {
async byIdAsync(
graphqlClient: ExpoGraphqlClient,
backgroundJobReceiptId: string
): Promise<BackgroundJobReceiptByIdQuery['backgroundJobReceipt']['byId'] | null> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BackgroundJobReceiptByIdQuery, BackgroundJobReceiptByIdQueryVariables>(
gql`
query BackgroundJobReceiptById($id: ID!) {
backgroundJobReceipt {
byId(id: $id) {
id
...BackgroundJobReceiptData
}
}
}
${BackgroundJobReceiptNode}
`,
{ id: backgroundJobReceiptId },
{
additionalTypenames: ['BackgroundJobReceipt'],
}
)
.toPromise()
);
return data.backgroundJobReceipt.byId ?? null;
},
};
@@ -7,6 +7,8 @@ import { withErrorHandlingAsync } from '../client';
import {
UpdateBranchBasicInfoFragment,
UpdateFragment,
ViewUpdateChannelBasicInfoOnAppQuery,
ViewUpdateChannelBasicInfoOnAppQueryVariables,
ViewUpdateChannelOnAppQuery,
ViewUpdateChannelOnAppQueryVariables,
ViewUpdateChannelsOnAppQuery,
@@ -41,6 +43,42 @@ export function composeUpdateBranchObject(
}
export const ChannelQuery = {
async viewUpdateChannelBasicInfoAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, channelName }: ViewUpdateChannelBasicInfoOnAppQueryVariables
): Promise<
NonNullable<ViewUpdateChannelBasicInfoOnAppQuery['app']['byId']['updateChannelByName']>
> {
const response = await withErrorHandlingAsync(
graphqlClient
.query<ViewUpdateChannelBasicInfoOnAppQuery, ViewUpdateChannelBasicInfoOnAppQueryVariables>(
gql`
query ViewUpdateChannelBasicInfoOnApp($appId: String!, $channelName: String!) {
app {
byId(appId: $appId) {
id
updateChannelByName(name: $channelName) {
id
...UpdateChannelBasicInfoFragment
}
}
}
}
${print(UpdateChannelBasicInfoFragmentNode)}
`,
{ appId, channelName },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
);
const { updateChannelByName } = response.app.byId;
if (!updateChannelByName) {
throw new ChannelNotFoundError(`Could not find channel with the name ${channelName}`);
}
return updateChannelByName;
},
async viewUpdateChannelAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, channelName, filter }: ViewUpdateChannelOnAppQueryVariables
@@ -0,0 +1,98 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
FingerprintFilterInput,
FingerprintFragment,
FingerprintsByAppIdQuery,
} from '../generated';
import { FingerprintFragmentNode } from '../types/Fingerprint';
export const FingerprintQuery = {
async byHashAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
hash,
}: {
appId: string;
hash: string;
}
): Promise<FingerprintFragment | null> {
const fingerprintConnection = await FingerprintQuery.getFingerprintsAsync(graphqlClient, {
appId,
fingerprintFilter: { hashes: [hash] },
first: 1,
});
const fingerprints = fingerprintConnection.edges.map(edge => edge.node);
return fingerprints[0] ?? null;
},
async getFingerprintsAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
first,
after,
last,
before,
fingerprintFilter,
}: {
appId: string;
first?: number;
after?: string;
last?: number;
before?: string;
fingerprintFilter?: FingerprintFilterInput;
}
): Promise<FingerprintsByAppIdQuery['app']['byId']['fingerprintsPaginated']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<FingerprintsByAppIdQuery>(
gql`
query FingerprintsByAppId(
$appId: String!
$after: String
$first: Int
$before: String
$last: Int
$fingerprintFilter: FingerprintFilterInput
) {
app {
byId(appId: $appId) {
id
fingerprintsPaginated(
after: $after
first: $first
before: $before
last: $last
filter: $fingerprintFilter
) {
edges {
node {
id
...FingerprintFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
}
${print(FingerprintFragmentNode)}
`,
{ appId, after, first, before, last, fingerprintFilter },
{ additionalTypenames: ['Fingerprint'] }
)
.toPromise()
);
return data.app?.byId.fingerprintsPaginated;
},
};
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff. Show more