Compare commits

..
76 Commits
Author SHA1 Message Date
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
Szymon Dziedzic 1fd14c07e1 v14.1.0 2024-12-10 11:24:35 +01:00
Szymon Dziedzic 39ea1705d8 [eas-cli] add support for broadcast push notifications option for push notifications capability (#2748)
* [eas-cli] add support for broadcast push notifications option for push notifications capability

* Apply suggestions from code review

* bump apple utils

* simplify logic

* add changelog

* add tests
2024-12-10 11:23:21 +01:00
dependabot[bot] 2c96b9929d Bump nanoid from 3.3.4 to 3.3.8 in /packages/eas-cli (#2754)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.4 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.4...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 10:01:17 +01:00
Szymon Dziedzic 0dae24ae47 [eas-cli] bump @expo/apple-utils to add fyi link for apple 2fa sms failure (#2752)
* [eas-cli] bump @expo/apple-utils to add fyi link for apple 2fa sms failure

* add changelog
2024-12-10 09:51:05 +01:00
Expo CI a63efe01a1 update CHANGELOG.md 2024-12-09 20:37:15 +00:00
Szymon Dziedzic 803457b37a v14.0.3 2024-12-09 20:57:35 +01:00
Evan Bacon dcb1305501 fix SMS TFA for Apple authentication (#2750)
* fix SMS TFA for Apple authentication

* Update CHANGELOG.md
2024-12-09 20:53:25 +01:00
Szymon Dziedzic 97fe2d62d2 add changelog entry 2024-12-06 15:41:15 +01:00
JonLuca De Caro ca0850b82b Change package.json update message to allow faster copy and paste (#2661) 2024-12-06 15:38:51 +01:00
Szymon Dziedzic 039a7b408b add changelog entry 2024-12-06 15:10:55 +01:00
Saiichi Shayan Hashimoto 68660ae845 Allow $schema in eas.json (#2624)
* Allow $schema in eas.json

* Add $schema to eas.schema.json
2024-12-06 15:08:11 +01:00
Expo CI 34bb2c8a75 update CHANGELOG.md 2024-12-06 12:48:28 +00:00
Szymon Dziedzic ac5cf3d7dd v14.0.2 2024-12-06 13:41:10 +01:00
Szymon Dziedzic 8d686b855b [eas-cli] make version/CFBundleShortVersionString validation logic show warn instead of throwing an error (#2741)
* [eas-cli] make version/CFBundleShortVersionString validation logic show warn instead of throwing an error

* fix tests

* add changelog entry
2024-12-06 13:39:52 +01:00
Expo CI 19973bb86f update CHANGELOG.md 2024-12-06 12:24:10 +00:00
Szymon Dziedzic d132ac09de v14.0.1 2024-12-06 13:18:24 +01:00
Szymon Dziedzic 4c5e5d4ded upgrade eas-cli-local-build-plugin to 1.0.152 2024-12-06 13:17:22 +01:00
Szymon Dziedzic 44af16efe1 bump @expo/apple-utils to improve error handling and input validation and change auth headers (#2745)
* bump @expo/apple-utils to improve error handling and input validation and change auth headers

* add changelog entry
2024-12-06 12:28:12 +01:00
Expo CI f34487cc8e update CHANGELOG.md 2024-12-06 09:10:27 +00:00
Szymon Dziedzic 7ea1c5de17 v14.0.0 2024-12-06 10:04:57 +01:00
Szymon Dziedzic 261f611770 [ENG-14410] update @expo/config and @expo/config-plugins to fix eas build command for bare iOS projects on Windows (#2744)
* [ENG-14410] update @expo/config and @expo/config-plugins to fix eas build command for bare iOS projects on Windows

* add changelog entry
2024-12-06 10:04:00 +01:00
Bartosz Kaszubowski f9337f419a [eas-cli] tweak message related to channel creation (#2735) 2024-12-05 12:01:57 +01:00
Szymon Dziedzic 4280e10836 [codemention] make codemention.yml up to date (#2740)
<!-- 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

It seems like this file includes redundant entries + is not up to date

# How

Remove redundant entries, add @radoslawkrzemien to global codeowners, make @wschurman and @quinlanj codeowners of everything EAS Update related (including channel commands), remove @byCedric and @EvanBacon from EAS Update codeowners

# Test Plan

Review
2024-12-05 10:16:58 +01:00
Quinlan Jung a2fe99c97e [eas-cli] fingerprint:compare (#2736)
* [eas-cli] fingerprint:compare

* Temporary Commit at 12/2/2024, 1:43:01 PM

* brent feedback

* Temporary Commit at 12/3/2024, 4:46:13 PM
2024-12-03 17:06:16 -08:00
Szymon Dziedzic 868fd399a4 [eas-cli] fix warning for gitignored google services file (#2730)
<!-- 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://discord.com/channels/695411232856997968/1309202098591633458/1311040361040183429

# How

Make warning up to date after recent env vars changes
2024-12-02 20:04:00 +01:00
Expo Bot 7d60ba2cea Upgrade eas-build dependencies (#2734)
⬆️ Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.
2024-12-02 13:23:37 +01:00
Will Schurman db7461a569 [eas-cli][eas-update] Change behavior of roll-back-to-embedded to not use current project state (#2722) 2024-11-25 10:56:10 -08:00
Expo CI facffbdcd7 update CHANGELOG.md 2024-11-25 11:57:06 +00:00
Szymon Dziedzic dbdefa0e3d v13.4.2 2024-11-25 12:51:08 +01:00
Szymon Dziedzic 66daeade84 [eas-cli] update expo packages to SDK 52 versions (#2706)
# Why

Update `@expo` packages to SDK 52 versions

# How

Update `@expo` packages to SDK 52 versions

Fixes #2696

It seems like the `privacy` field was removed from the expo config in the SDK 52 update: https://expo.dev/changelog/2024/11-12-sdk-52. 

I used `AppPrivay.Unlisted` for the `createApp` mutation input for which this is still required. It was an option selected for an app when `exp.privacy` was undefined.

It also seems that `findWorkspaceRoot` function in `@expo/package-manager` was changed to `resolveWorkspaceRoot`.

# Test Plan

Tests, test commands manually
2024-11-25 12:44:30 +01:00
Expo CI 48ebd9bb78 update CHANGELOG.md 2024-11-22 15:17:53 +00:00
Szymon Dziedzic 3d3574d351 v13.4.1 2024-11-22 15:59:13 +01:00
Szymon Dziedzic 8da6adf4da [eas-cli] improve eas env:pull logging (#2720)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

# How

Improve logs

# Test Plan

Tests
2024-11-22 15:58:28 +01:00
Expo CI bd2d39f26b update CHANGELOG.md 2024-11-22 13:12:02 +00:00
Szymon Dziedzic e16e1f3871 v13.4.0 2024-11-22 14:05:01 +01:00
Szymon Dziedzic d8bd6a1548 [eas-cli] add environment to eas.schema.json (#2719)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

Add environment to `eas.schema.json`

# How

Add environment to `eas.schema.json`
2024-11-22 14:03:07 +01:00
Szymon Dziedzic 0f2fad3a65 [ENG-13252][eas-cli] resolve default environment automatically based on build configuration (#2644)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C06FK950085/p1729212611408479?thread_ts=1729186741.835629&cid=C06FK950085

# How

Resolve the default (if not set in the build profile) `environment` to use for a build based on its configuration.

Store build -> `production`
Dev client -> `development`
Other (`internal`) -> `preview`

# Test Plan

Test manually
2024-11-22 11:33:11 +01:00
Szymon Dziedzic 78e05c48c8 [eas-cli] make eas env flags visibile (#2717)
<!-- 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

Make `eas env` commands visible

# How

Make `eas env` commands visible

# Test Plan

Tests
2024-11-22 10:41:17 +01:00
Szymon Dziedzic fa10404843 [ENG-14249][eas-cli] deprecate eas secret commands (#2705)
<!-- 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

Deprecate old secret commands

# How

Mark as hidden + print warn when used

# Test Plan

Tests
2024-11-22 10:27:42 +01:00
Piotr Szeremeta 5bf2c3a65b All account-wide envvars should be global (for now) (#2713) 2024-11-21 09:49:57 +01:00
Quinlan Jung ec71e1168f [eas-cli] pass on fingerprint info on republish (#2708)
# Why

This PR adds support for fingerprint information in update republishing workflows. If the republished update has fingerprint info, we include it. 

# Before landing

- [ ] merge https://github.com/expo/universe/pull/17575
- [ ] rerun graphql generation against staging

# How

- Added fingerprint fields to the GraphQL schema and related types
- Updated the Update fragment to include fingerprint information (id, hash, and source details)
- Modified the republish functionality to include fingerprint information when republishing updates
- Added support for fingerprint source types and bucket key information

# Test Plan

- [x] republished update with fingerprint info https://staging.expo.dev/accounts/quintest110/projects/staging-sdk-52/updates/dcf49334-3330-4d37-8ef5-50c2b7bea414
- [x] republished update without fingerprint info https://staging.expo.dev/accounts/quintest110/projects/staging-sdk-52/updates/fd7f062b-cc2e-4c54-b306-36587dc841f6
2024-11-20 13:44:17 -08:00
Quinlan Jung 8bf28029dc [eas-cli] create fingerprint on each update (#2687)
# Why

This PR computes a fingerprint for each update. 
- If the update does not use the fingerprint policy, it calculates the fingerprint using the exported cli in the `expo` package for projects SDK 52+. 
- If the update uses the fingerprint policy, it just uses the fingerprint that was previously calculated using `expo-updates`.  

# How

- Modified fingerprint computation to handle platform-specific cases
- Updated the fingerprint data structure to use `fingerprintHash` instead of the full fingerprint object
- Added support for computing fingerprints for projects without expo-updates
- Introduced new types and interfaces for handling fingerprint information groups
- Created utility functions for async map operations and fingerprint calculations
- Updated GraphQL schema to support new fingerprint-related fields

# Test Plan

- [x] on build: verify fingerprints till work 
- [x] on update: verify fingerprint is calculated for new project with SDK 52
- [x] on update: verify no fingerprint is calculated for old project (< SDK 52)
- [x] on update: verify fingerprint is reused for project with fingerprint runtime policy
2024-11-20 13:43:06 -08:00
Szymon Dziedzic fc3c1e73a8 [ENG-14223][eas-cli] improve class names and descriptions of env commands (#2711)
<!-- 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

Make class names more consistent with other commands

Improve descriptions

# How

Make class names more consistent with other commands

Improve descriptions

# Test Plan

Tests
2024-11-20 17:51:15 +01:00
Szymon Dziedzic bb3915dc7e [ENG-14185][eas-cli] use account-wide vs shared in env commands (#2710)
<!-- 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

Stick to account-wide everywhere for now

# How

Rename shared to account-wide, fix some bugs and type issues

# Test Plan

Tests
2024-11-20 17:34:26 +01:00
Piotr Szeremeta 753df2c428 Environment Variables: Rename encrypted to secret (#2709) 2024-11-20 14:50:14 +01:00
Expo Bot 3111492f83 Upgrade eas-build dependencies (#2703)
⬆️ Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.
2024-11-19 12:43:29 +01:00
Expo CI 9c95755a46 update CHANGELOG.md 2024-11-18 21:42:12 +00:00
108 changed files with 4163 additions and 1500 deletions

No files matched your search

+6 -12
View File
@@ -3,24 +3,18 @@ commentConfiguration:
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/build/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/commands/build/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/submit/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/commands/submit.ts']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela', 'radoslawkrzemien']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['EvanBacon', 'byCedric', 'wschurman']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/update/**']
mentions: ['EvanBacon', 'byCedric', 'wschurman']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/channel/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/channel/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/schema/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/commands/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-json/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
+129 -1
View File
@@ -8,12 +8,140 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Added `eas workflow` commands. [#2650](https://github.com/expo/eas-cli/pull/2650), [#2669](https://github.com/expo/eas-cli/pull/2669), [#2678](https://github.com/expo/eas-cli/pull/2678) by [@jonsamp](https://github.com/jonsamp) and [@sjchmiela](https://github.com/sjchmiela) ([#2683](https://github.com/expo/eas-cli/pull/2683) by [@sjchmiela](https://github.com/sjchmiela))
- 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))
### 🐛 Bug fixes
### 🧹 Chores
## [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))
## [14.0.3](https://github.com/expo/eas-cli/releases/tag/v14.0.3) - 2024-12-09
### 🐛 Bug fixes
- Bump `@expo/apple-utils` to fix sending two-factor authentication codes via SMS. ([#2750](https://github.com/expo/eas-cli/pull/2750) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Change update message to allow faster copy and paste. ([#2661](https://github.com/expo/eas-cli/pull/2661) by [@jonluca](https://github.com/jonluca))
- Allow using `$schema` field in `eas.json`. ([#2624](https://github.com/expo/eas-cli/pull/2624) by [@saiichihashimoto](https://github.com/saiichihashimoto))
## [14.0.2](https://github.com/expo/eas-cli/releases/tag/v14.0.2) - 2024-12-06
### 🧹 Chores
- Print warning instead of throwing an error when validating `CFBundleShortVersionString` against App Store requirements. ([#2741](https://github.com/expo/eas-cli/pull/2741) by [@szdziedzic](https://github.com/szdziedzic))
## [14.0.1](https://github.com/expo/eas-cli/releases/tag/v14.0.1) - 2024-12-06
### 🧹 Chores
- Bump `@expo/apple-utils` to improve error handling and input validation and change auth headers. ([#2745](https://github.com/expo/eas-cli/pull/2745) by [@szdziedzic](https://github.com/szdziedzic))
## [14.0.0](https://github.com/expo/eas-cli/releases/tag/v14.0.0) - 2024-12-06
### 🛠 Breaking changes
- Change behavior of roll-back-to-embedded to not use current project state. ([#2722](https://github.com/expo/eas-cli/pull/2722) by [@wschurman](https://github.com/wschurman))
### 🎉 New features
- Add hidden fingerprint:compare feature. ([#2736](https://github.com/expo/eas-cli/pull/2736) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Update `@expo/config` and `@expo/config-plugins` to fix `eas build` command for bare iOS projects on Windows. ([#2744](https://github.com/expo/eas-cli/pull/2744) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Fix outdated gitignored Google services file warning. ([#2730](https://github.com/expo/eas-cli/pull/2730) by [@szdziedzic](https://github.com/szdziedzic))
## [13.4.2](https://github.com/expo/eas-cli/releases/tag/v13.4.2) - 2024-11-25
### 🧹 Chores
- Upgrade `@expo` packages to SDK 52 versions. ([#2706](https://github.com/expo/eas-cli/pull/2706) by [@szdziedzic](https://github.com/szdziedzic))
## [13.4.1](https://github.com/expo/eas-cli/releases/tag/v13.4.1) - 2024-11-22
### 🧹 Chores
- Improve logging in `eas env:pull`. ([#2720](https://github.com/expo/eas-cli/pull/2720) by [@szdziedzic](https://github.com/szdziedzic))
## [13.4.0](https://github.com/expo/eas-cli/releases/tag/v13.4.0) - 2024-11-22
### 🎉 New features
- Calculate fingerprint on each update. ([#2687](https://github.com/expo/eas-cli/pull/2687) by [@quinlanj](https://github.com/quinlanj))
- Calculate fingerprint on each update republish. ([#2708](https://github.com/expo/eas-cli/pull/2708) by [@quinlanj](https://github.com/quinlanj))
- Add `eas env` commands. ([#2711](https://github.com/expo/eas-cli/pull/2711) by [@szdziedzic](https://github.com/szdziedzic))
- Add `--environment` flag to `eas update` command. ([#2711](https://github.com/expo/eas-cli/pull/2711) by [@szdziedzic](https://github.com/szdziedzic))
- Load readable environment variables from EAS servers on every build. ([#2644](https://github.com/expo/eas-cli/pull/2644) by [@szdziedzic](https://github.com/szdziedzic))
- Add environment field to `eas.schema.json`. ([#2719](https://github.com/expo/eas-cli/pull/2719) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Deprecate `eas secret` commands. ([#2705](https://github.com/expo/eas-cli/pull/2705) by [@szdziedzic](https://github.com/szdziedzic))
## [13.3.0](https://github.com/expo/eas-cli/releases/tag/v13.3.0) - 2024-11-18
### 🎉 New features
- Added `eas workflow` commands. [#2650](https://github.com/expo/eas-cli/pull/2650), [#2669](https://github.com/expo/eas-cli/pull/2669), [#2678](https://github.com/expo/eas-cli/pull/2678) by [@jonsamp](https://github.com/jonsamp) and [@sjchmiela](https://github.com/sjchmiela) ([#2683](https://github.com/expo/eas-cli/pull/2683) by [@sjchmiela](https://github.com/sjchmiela))
## [13.2.3](https://github.com/expo/eas-cli/releases/tag/v13.2.3) - 2024-11-15
## [13.2.2](https://github.com/expo/eas-cli/releases/tag/v13.2.2) - 2024-11-15
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "13.3.0",
"version": "14.4.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+432 -168
View File
File diff suppressed because it is too large. Load diff
+905 -293
View File
File diff suppressed because it is too large. Load diff
+22 -16
View File
@@ -1,34 +1,34 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "13.3.0",
"version": "14.4.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "2.1.0",
"@expo/apple-utils": "2.1.5",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "9.0.4",
"@expo/config-plugins": "8.0.10",
"@expo/eas-build-job": "1.0.149",
"@expo/eas-json": "13.2.2",
"@expo/env": "^0.3.0",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.156",
"@expo/eas-json": "14.3.1",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.117",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.1.3",
"@expo/package-manager": "1.5.2",
"@expo/pkcs12": "0.1.2",
"@expo/plist": "0.1.3",
"@expo/multipart-body-parser": "2.0.0",
"@expo/osascript": "2.1.4",
"@expo/package-manager": "1.7.0",
"@expo/pkcs12": "0.1.3",
"@expo/plist": "0.2.0",
"@expo/plugin-help": "5.1.23",
"@expo/plugin-warn-if-update-available": "2.5.1",
"@expo/prebuild-config": "7.0.9",
"@expo/prebuild-config": "8.0.17",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.149",
"@expo/steps": "1.0.156",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -41,6 +41,7 @@
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"dateformat": "4.6.3",
"diff": "7.0.0",
"dotenv": "16.3.1",
"env-paths": "2.2.0",
"envinfo": "7.11.0",
@@ -64,7 +65,7 @@
"mime": "3.0.0",
"minimatch": "5.1.2",
"minizlib": "3.0.1",
"nanoid": "3.3.4",
"nanoid": "3.3.8",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"nullthrows": "1.1.1",
@@ -94,8 +95,10 @@
"@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",
"@types/envinfo": "7.8.3",
"@types/express": "4.17.15",
"@types/fs-extra": "11.0.4",
@@ -177,6 +180,9 @@
"secret": {
"description": "manage project and account secrets"
},
"env": {
"description": "manage project and account environment variables"
},
"update": {
"description": "manage individual updates"
},
@@ -189,7 +195,7 @@
},
"warn-if-update-available": {
"timeoutInDays": 0.5,
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run:') %>\n<%= chalk.dim(chalk.bold('npm install -g eas-cli')) %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
},
"update": {
"node": {
+11 -8
View File
@@ -1,4 +1,5 @@
import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { BranchNotFoundError } from './utils';
@@ -8,10 +9,11 @@ import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranch,
UpdateBranchBasicInfoFragment,
UpdateBranchFragment,
} from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import { UpdateBranchBasicInfoFragmentNode } from '../graphql/types/UpdateBranchBasicInfo';
import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
@@ -126,7 +128,7 @@ function renderPageOfBranches(
export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
): Promise<UpdateBranchBasicInfoFragment> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
@@ -135,15 +137,17 @@ export async function createUpdateBranchOnAppAsync(
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
name
...UpdateBranchBasicInfoFragment
}
}
}
${print(UpdateBranchBasicInfoFragmentNode)}
`,
{
appId,
name,
}
},
{ additionalTypenames: ['UpdateBranch'] }
)
.toPromise()
);
@@ -163,22 +167,21 @@ export async function ensureBranchExistsAsync(
appId: string;
branchName: string;
}
): Promise<{ branchId: string; createdBranch: boolean }> {
): Promise<{ branch: UpdateBranchBasicInfoFragment; createdBranch: boolean }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
const { id } = updateBranch;
return { branchId: id, createdBranch: false };
return { branch: updateBranch, createdBranch: false };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id, createdBranch: true };
return { branch: newUpdateBranch, createdBranch: true };
} else {
throw error;
}
+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 {
@@ -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),
+4 -7
View File
@@ -671,7 +671,7 @@ async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends
}> {
const runtimeAndFingerprintMetadata =
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
if (!runtimeAndFingerprintMetadata?.fingerprint) {
if (!runtimeAndFingerprintMetadata?.fingerprintHash) {
const fingerprint = await computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync(ctx);
return {
...runtimeAndFingerprintMetadata,
@@ -690,10 +690,7 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
): Promise<{
runtimeVersion?: string;
fingerprintSource?: FingerprintSource;
fingerprint?: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
};
fingerprintHash?: string;
}> {
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
exp: ctx.exp,
@@ -727,7 +724,7 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
return {
runtimeVersion: uploadedFingerprint.hash,
fingerprintSource: uploadedFingerprint.fingerprintSource,
fingerprint: resolvedRuntimeVersion.fingerprint,
fingerprintHash: resolvedRuntimeVersion.fingerprintHash ?? undefined,
};
}
@@ -740,7 +737,7 @@ async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends
}> {
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
workflow: ctx.workflow,
platform: ctx.platform,
platforms: [ctx.platform],
env: ctx.env,
});
if (!fingerprint) {
@@ -54,9 +54,10 @@ async function resolveEnvVarsAsync({
projectId: string;
}): Promise<Env> {
const environment =
buildProfile.environment?.toUpperCase() ?? process.env.EAS_CURRENT_ENVIRONMENT;
buildProfile.environment?.toUpperCase() ??
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
if (!environment || !isEnvironment(environment)) {
if (!isEnvironment(environment)) {
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
buildProfile.env && Object.keys(buildProfile.env).length > 0
@@ -130,3 +131,27 @@ async function resolveEnvVarsAsync({
return { ...buildProfile.env };
}
}
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;
}
}
@@ -5,7 +5,6 @@ import { vol } from 'memfs';
import os from 'os';
import type { XCBuildConfiguration } from 'xcode';
import { learnMore } from '../../../log';
import { readPlistAsync } from '../../../utils/plist';
import { resolveVcsClient } from '../../../vcs';
import {
@@ -301,27 +300,6 @@ describe(readShortVersionAsync, () => {
);
expect(buildNumber).toBe('1.0.0');
});
it('fails when build number is invalid', async () => {
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
await expect(
readShortVersionAsync(
'/app',
exp,
{
CURRENT_PROJECT_VERSION: '0.0.7.1.028',
},
vcsClient
)
).rejects.toThrowError(
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: 0.0.7.1.028. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
});
});
describe('managed project', () => {
+4 -4
View File
@@ -109,14 +109,14 @@ function validateShortVersion({
}): void {
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
if (workflow === Workflow.MANAGED) {
throw new Error(
`The required format for "version" field from app.json/app.config.ts is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the required format. ${learnMore(
Log.warn(
`The required format for "version" field from app.json/app.config.ts for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the format required by Apple. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
} else {
throw new Error(
`The required format for "CFBundleShortVersionString" in Info.plist is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the required format. ${learnMore(
Log.warn(
`The required format for "CFBundleShortVersionString" in Info.plist for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the format required by Apple. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
+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.144';
const PLUGIN_PACKAGE_VERSION = '1.0.152';
export enum LocalBuildMode {
/**
+10 -6
View File
@@ -27,24 +27,28 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const googleServicesEnvVar =
ctx.platform === Platform.ANDROID
? ctx.env.GOOGLE_SERVICES_JSON
: ctx.env.GOOGLE_SERVICES_INFO_PLIST;
const rootDir = path.normalize(await ctx.vcsClient.getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
(await ctx.vcsClient.isFileIgnoredAsync(
path.relative(rootDir, absGoogleServicesFilePath)
))) &&
!googleServicesEnvVar
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
);
Log.warn(
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
`Use EAS file environment variables with secret or sensitive visibility to pass all values that you don't want to include in your version control to build process. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
)}`
);
Log.warn(
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
);
Log.newLine();
}
}
+21 -24
View File
@@ -19,7 +19,7 @@ import {
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
import Log from '../log';
import Log, { learnMore } from '../log';
import { getDisplayNameForProjectIdAsync } from '../project/projectUtils';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
@@ -303,7 +303,6 @@ export async function doesChannelExistAsync(
}
/**
*
* Creates a channel and links it to a branch with the same name.
*
* @param appId the app ID, also known as the project ID
@@ -319,7 +318,7 @@ export async function createAndLinkChannelAsync(
}: { appId: string; channelName: string; shouldPrintJson?: boolean }
): Promise<ChannelBasicInfo> {
let branchId: string;
let branchMessage: string;
let hasCreatedBranch = false;
try {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
@@ -327,7 +326,6 @@ export async function createAndLinkChannelAsync(
name: channelName,
});
branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
@@ -335,7 +333,7 @@ export async function createAndLinkChannelAsync(
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
hasCreatedBranch = true;
} else {
throw error;
}
@@ -358,26 +356,25 @@ export async function createAndLinkChannelAsync(
if (shouldPrintJson) {
printJsonOnlyOutput(newChannel);
} else {
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, appId);
const learnMoreLink = learnMore('https://docs.expo.dev/eas-update/eas-cli/');
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(
await getDisplayNameForProjectIdAsync(graphqlClient, appId)
)}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
if (hasCreatedBranch) {
Log.withTick(
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} and branch ${chalk.bold(
`"${newChannel.name}"`
)} on ${chalk.bold(projectDisplayName)} project. ${learnMoreLink}`
);
} else {
Log.withTick(
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} on ${chalk.bold(
projectDisplayName
)} project and connected it with existing ${chalk.bold(
`"${newChannel.name}"`
)} branch. ${learnMoreLink}`
);
}
}
return newChannel;
}
+9 -1
View File
@@ -38,7 +38,12 @@ export async function fetchBuildsAsync({
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
filters?: { statuses?: BuildStatus[]; platform?: RequestedPlatform; profile?: string };
filters?: {
statuses?: BuildStatus[];
platform?: RequestedPlatform;
profile?: string;
hasFingerprint?: boolean;
};
}): Promise<BuildFragment[]> {
let builds: BuildFragment[];
const queryFilters: InputMaybe<BuildFilter> = {};
@@ -48,6 +53,9 @@ export async function fetchBuildsAsync({
if (filters?.profile) {
queryFilters['buildProfile'] = filters.profile;
}
if (filters?.hasFingerprint) {
queryFilters['hasFingerprint'] = filters.hasFingerprint;
}
if (!filters?.statuses) {
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
@@ -225,7 +225,7 @@ describe(getProjectIdAsync, () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
config: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } },
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
@@ -42,7 +42,7 @@ async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
}
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
const maybeRepoRoot = PackageManagerUtils.resolveWorkspaceRoot(projectDir) ?? projectDir;
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
@@ -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;
}
@@ -12,7 +12,6 @@ import {
getPrivateExpoConfigAsync,
} from '../../../project/expoConfig';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../../project/projectUtils';
import SessionManager from '../../../user/SessionManager';
import { Actor, getActorUsername } from '../../../user/User';
@@ -197,7 +196,6 @@ export async function validateOrSetProjectIdAsync({
{
accountName: getAccountNameForEASProjectSync(exp, actor),
projectName: exp.slug,
privacy: toAppPrivacy(exp.privacy),
},
{
nonInteractive: options.nonInteractive,
@@ -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();
+10 -10
View File
@@ -1,6 +1,6 @@
import { Flags } from '@oclif/core';
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../graphql/generated';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
// are used in tandem, it saves on unnecessary typying in commands
@@ -52,18 +52,19 @@ export const EASVariableFormatFlag = {
};
export const EASVariableVisibilityFlag = {
visibility: Flags.enum<'plaintext' | 'sensitive' | 'encrypted'>({
visibility: Flags.enum<'plaintext' | 'sensitive' | 'secret'>({
description: 'Visibility of the variable',
options: ['plaintext', 'sensitive', 'encrypted'],
options: ['plaintext', 'sensitive', 'secret'],
}),
};
export const EASVariableScopeFlag = {
scope: Flags.enum<EnvironmentVariableScope>({
export type EASEnvironmentVariableScopeFlagValue = 'project' | 'account';
export const EASEnvironmentVariableScopeFlag = {
scope: Flags.enum<EASEnvironmentVariableScopeFlagValue>({
description: 'Scope for the variable',
options: mapToLowercase([EnvironmentVariableScope.Shared, EnvironmentVariableScope.Project]),
parse: upperCaseAsync,
default: EnvironmentVariableScope.Project,
options: ['project', 'account'],
default: 'project',
}),
};
@@ -82,11 +83,10 @@ export const EasJsonOnlyFlag = {
export const EasUpdateEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
description:
'Environment to use the server-side defined EAS environment variables for during command execution.',
'Environment to use for the server-side defined EAS environment variables during command execution.',
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
parse: upperCaseAsync,
required: false,
hidden: true,
default: null,
}),
};
@@ -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),
}));
}
@@ -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;
}
@@ -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;
@@ -19,14 +19,14 @@ import {
promptVariableTypeAsync,
promptVariableValueAsync,
} from '../../../utils/prompts';
import EnvironmentVariableCreate from '../create';
import EnvCreate from '../create';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvironmentVariableCreate, () => {
describe(EnvCreate, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const variableId = 'testId';
@@ -74,7 +74,7 @@ describe(EnvironmentVariableCreate, () => {
describe('in interactive mode', () => {
it('creates a project variable', async () => {
const command = new EnvironmentVariableCreate(
const command = new EnvCreate(
[
'--name',
'VarName',
@@ -83,7 +83,7 @@ describe(EnvironmentVariableCreate, () => {
'--environment',
'production',
'--visibility',
'encrypted',
'secret',
],
mockConfig
);
@@ -110,7 +110,7 @@ describe(EnvironmentVariableCreate, () => {
});
it('updates an existing variable in the same environment', async () => {
const command = new EnvironmentVariableCreate(
const command = new EnvCreate(
[
'--name',
'VarName',
@@ -119,7 +119,7 @@ describe(EnvironmentVariableCreate, () => {
'--environment',
'production',
'--visibility',
'encrypted',
'secret',
],
mockConfig
);
@@ -171,8 +171,8 @@ describe(EnvironmentVariableCreate, () => {
});
});
it('creates a shared variable', async () => {
const command = new EnvironmentVariableCreate(
it('creates an account-wide variable', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
@@ -181,7 +181,7 @@ describe(EnvironmentVariableCreate, () => {
'--environment',
'production',
'--scope',
'shared',
'account',
],
mockConfig
);
@@ -199,6 +199,7 @@ describe(EnvironmentVariableCreate, () => {
{
name: 'VarName',
value: 'VarValue',
isGlobal: true,
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
@@ -207,8 +208,8 @@ describe(EnvironmentVariableCreate, () => {
);
});
it('throws if a shared variable already exists', async () => {
const command = new EnvironmentVariableCreate(
it('throws if an account-wide variable already exists', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
@@ -217,7 +218,7 @@ describe(EnvironmentVariableCreate, () => {
'--environment',
'production',
'--scope',
'shared',
'account',
],
mockConfig
);
@@ -242,8 +243,8 @@ describe(EnvironmentVariableCreate, () => {
await expect(command.runAsync()).rejects.toThrow();
});
it('updates if a shared variable already exists and --force flag is set', async () => {
const command = new EnvironmentVariableCreate(
it('updates if an account-wide variable already exists and --force flag is set', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
@@ -253,7 +254,7 @@ describe(EnvironmentVariableCreate, () => {
'production',
'--force',
'--scope',
'shared',
'account',
],
mockConfig
);
@@ -288,8 +289,8 @@ describe(EnvironmentVariableCreate, () => {
});
});
it('creates a shared variable and links it', async () => {
const command = new EnvironmentVariableCreate(
it('creates an account-wide variable and links it', async () => {
const command = new EnvCreate(
[
'--name',
'VarName',
@@ -300,7 +301,7 @@ describe(EnvironmentVariableCreate, () => {
'--environment',
'development',
'--scope',
'shared',
'account',
'--link',
],
mockConfig
@@ -324,6 +325,7 @@ describe(EnvironmentVariableCreate, () => {
EnvironmentVariableEnvironment.Development,
],
visibility: EnvironmentVariableVisibility.Public,
isGlobal: true,
type: EnvironmentSecretType.String,
},
testAccountId
@@ -343,7 +345,7 @@ describe(EnvironmentVariableCreate, () => {
});
it('prompts for missing arguments', async () => {
const command = new EnvironmentVariableCreate([], mockConfig);
const command = new EnvCreate([], mockConfig);
jest.mocked(promptVariableNameAsync).mockImplementation(async () => 'VarName');
jest.mocked(promptVariableValueAsync).mockImplementation(async () => 'VarValue');
@@ -8,14 +8,14 @@ import { EnvironmentVariableMutation } from '../../../graphql/mutations/Environm
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableDelete from '../delete';
import EnvDelete from '../delete';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../log');
describe(EnvironmentVariableDelete, () => {
describe(EnvDelete, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
@@ -40,7 +40,7 @@ describe(EnvironmentVariableDelete, () => {
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(
const command = new EnvDelete(
[
'--variable-name',
'TEST_VARIABLE',
@@ -66,7 +66,7 @@ describe(EnvironmentVariableDelete, () => {
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableDelete([], mockConfig);
const command = new EnvDelete([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
@@ -82,10 +82,7 @@ describe(EnvironmentVariableDelete, () => {
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
const command = new EnvDelete(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
@@ -99,7 +96,7 @@ describe(EnvironmentVariableDelete, () => {
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
const command = new EnvDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
@@ -116,7 +113,7 @@ describe(EnvironmentVariableDelete, () => {
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(false);
const command = new EnvironmentVariableDelete(['--non-interactive'], mockConfig);
const command = new EnvDelete(['--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
@@ -12,14 +12,14 @@ import {
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../../utils/prompts';
import EnvironmentVariableGet from '../get';
import EnvGet from '../get';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvironmentVariableGet, () => {
describe(EnvGet, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const mockVariables: EnvironmentVariableFragment[] = [
@@ -42,7 +42,7 @@ describe(EnvironmentVariableGet, () => {
.mockResolvedValueOnce(mockVariables);
jest.spyOn(Log, 'log').mockImplementation(() => {});
const command = new EnvironmentVariableGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
const command = new EnvGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -68,7 +68,7 @@ describe(EnvironmentVariableGet, () => {
const errorMessage =
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag";
const command = new EnvironmentVariableGet([], mockConfig);
const command = new EnvGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -85,7 +85,7 @@ describe(EnvironmentVariableGet, () => {
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
const command = new EnvironmentVariableGet([], mockConfig);
const command = new EnvGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -11,7 +11,7 @@ import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableLink from '../link';
import EnvLink from '../link';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
@@ -19,7 +19,7 @@ jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableLink, () => {
describe(EnvLink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
@@ -39,7 +39,7 @@ describe(EnvironmentVariableLink, () => {
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('links a shared variable to the current project in non-interactive mode', async () => {
it('links an account-wide variable to the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
@@ -53,7 +53,7 @@ describe(EnvironmentVariableLink, () => {
mockVariables[0]
);
const command = new EnvironmentVariableLink(
const command = new EnvLink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
@@ -75,7 +75,7 @@ describe(EnvironmentVariableLink, () => {
);
});
it('links a shared variable to the current project to a specified environment', async () => {
it('links an account-wide variable to the current project to a specified environment', async () => {
const mockVariables = [
{
id: variableId,
@@ -89,7 +89,7 @@ describe(EnvironmentVariableLink, () => {
mockVariables[0]
);
const command = new EnvironmentVariableLink(
const command = new EnvLink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
@@ -137,7 +137,7 @@ describe(EnvironmentVariableLink, () => {
);
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink(
const command = new EnvLink(
[
'--variable-name',
'TEST_VARIABLE',
@@ -186,7 +186,7 @@ describe(EnvironmentVariableLink, () => {
(promptAsync as jest.Mock).mockResolvedValue({ environments: mockVariables[0].environments });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink([], mockConfig);
const command = new EnvLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
@@ -207,15 +207,12 @@ describe(EnvironmentVariableLink, () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableLink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
@@ -239,7 +236,7 @@ describe(EnvironmentVariableLink, () => {
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink([], mockConfig);
const command = new EnvLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
@@ -263,15 +260,12 @@ describe(EnvironmentVariableLink, () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableLink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -13,7 +13,7 @@ import {
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import EnvironmentVariableList from '../list';
import EnvList from '../list';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/queries/AppQuery');
@@ -44,7 +44,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
},
];
describe(EnvironmentVariableList, () => {
describe(EnvList, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
@@ -56,7 +56,7 @@ describe(EnvironmentVariableList, () => {
it('lists project environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList([], mockConfig);
const command = new EnvList([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -77,7 +77,7 @@ describe(EnvironmentVariableList, () => {
it('lists project environment variables in specified environments', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--environment', 'production'], mockConfig);
const command = new EnvList(['--environment', 'production'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -100,7 +100,7 @@ describe(EnvironmentVariableList, () => {
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--include-sensitive'], mockConfig);
const command = new EnvList(['--include-sensitive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -121,10 +121,10 @@ describe(EnvironmentVariableList, () => {
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists shared environment variables successfully', async () => {
it('lists account-wide environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--scope', 'shared'], mockConfig);
const command = new EnvList(['--scope', 'account'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -142,15 +142,12 @@ describe(EnvironmentVariableList, () => {
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists shared environment variables including sensitive values', async () => {
it('lists account-wide environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.sharedWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(
['--include-sensitive', '--scope', 'shared'],
mockConfig
);
const command = new EnvList(['--include-sensitive', '--scope', 'account'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -11,7 +11,7 @@ import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableUnlink from '../unlink';
import EnvUnlink from '../unlink';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
@@ -19,7 +19,7 @@ jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableUnlink, () => {
describe(EnvUnlink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
@@ -39,7 +39,7 @@ describe(EnvironmentVariableUnlink, () => {
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('unlinks a shared variable from the current project in non-interactive mode', async () => {
it('unlinks an account-wide variable from the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
@@ -54,7 +54,7 @@ describe(EnvironmentVariableUnlink, () => {
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUnlink(
const command = new EnvUnlink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
@@ -76,7 +76,7 @@ describe(EnvironmentVariableUnlink, () => {
);
});
it('unlinks a shared variable from the current project in a specified environment', async () => {
it('unlinks an account-wide variable from the current project in a specified environment', async () => {
const mockVariables = [
{
id: variableId,
@@ -91,7 +91,7 @@ describe(EnvironmentVariableUnlink, () => {
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUnlink(
const command = new EnvUnlink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
@@ -141,7 +141,7 @@ describe(EnvironmentVariableUnlink, () => {
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUnlink([], mockConfig);
const command = new EnvUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
@@ -162,15 +162,12 @@ describe(EnvironmentVariableUnlink, () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
@@ -194,7 +191,7 @@ describe(EnvironmentVariableUnlink, () => {
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUnlink([], mockConfig);
const command = new EnvUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
@@ -218,15 +215,12 @@ describe(EnvironmentVariableUnlink, () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -12,7 +12,7 @@ import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableUpdate from '../update';
import EnvUpdate from '../update';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
@@ -20,7 +20,7 @@ jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableUpdate, () => {
describe(EnvUpdate, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
@@ -47,7 +47,7 @@ describe(EnvironmentVariableUpdate, () => {
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUpdate(
const command = new EnvUpdate(
[
'--variable-name',
'TEST_VARIABLE',
@@ -81,7 +81,7 @@ describe(EnvironmentVariableUpdate, () => {
);
});
it('updates a shared variable by selected name in non-interactive mode', async () => {
it('updates an account-wide variable by selected name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
@@ -93,7 +93,7 @@ describe(EnvironmentVariableUpdate, () => {
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUpdate(
const command = new EnvUpdate(
[
'--variable-name',
'TEST_VARIABLE',
@@ -107,7 +107,7 @@ describe(EnvironmentVariableUpdate, () => {
'--environment',
'production',
'--scope',
'shared',
'account',
],
mockConfig
);
@@ -135,7 +135,7 @@ describe(EnvironmentVariableUpdate, () => {
(promptAsync as jest.Mock).mockResolvedValue({ name: mockVariables[0].name });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUpdate([], mockConfig);
const command = new EnvUpdate([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
@@ -154,10 +154,7 @@ describe(EnvironmentVariableUpdate, () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUpdate(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
const command = new EnvUpdate(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnvDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with --variable-name VARIABLE_NAME flag to fix the issue"`;
@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnvironmentVariableDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with --variable-name VARIABLE_NAME flag to fix the issue"`;
+44 -36
View File
@@ -5,9 +5,10 @@ import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EASVariableVisibilityFlag,
} from '../../commandUtils/flags';
import {
@@ -34,23 +35,32 @@ import {
} from '../../utils/prompts';
import { isEnvironment, performForEnvironmentsAsync } from '../../utils/variableUtils';
type CreateFlags = {
interface RawCreateFlags {
name?: string;
value?: string;
link?: boolean;
force?: boolean;
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'encrypted';
scope?: EnvironmentVariableScope;
visibility?: 'plaintext' | 'sensitive' | 'secret';
scope: EASEnvironmentVariableScopeFlagValue;
environment?: EnvironmentVariableEnvironment[];
'non-interactive': boolean;
};
}
export default class EnvironmentVariableCreate extends EasCommand {
static override description =
'create an environment variable on the current project or owner account';
interface CreateFlags {
name?: string;
value?: string;
link?: boolean;
force?: boolean;
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
scope: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment[];
'non-interactive': boolean;
}
static override hidden = true;
export default class EnvCreate extends EasCommand {
static override description = 'create an environment variable for the current project or account';
static override args = [
{
@@ -69,7 +79,8 @@ export default class EnvironmentVariableCreate extends EasCommand {
description: 'Text value or the variable',
}),
link: Flags.boolean({
description: 'Link shared variable to the current project',
description: 'Link account-wide variable to the current project',
hidden: true, // every account-wide variable is global for now so it's not user facing
}),
force: Flags.boolean({
description: 'Overwrite existing variable',
@@ -80,7 +91,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
options: ['string', 'file'],
}),
...EASVariableVisibilityFlag,
...EASVariableScopeFlag,
...EASEnvironmentVariableScopeFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -92,9 +103,9 @@ export default class EnvironmentVariableCreate extends EasCommand {
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentVariableCreate);
const { args, flags } = await this.parse(EnvCreate);
const validatedFlags = this.validateFlags(flags);
const validatedFlags = this.sanitizeFlags(flags);
const {
name,
@@ -112,7 +123,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariableCreate, {
} = await this.getContextAsync(EnvCreate, {
nonInteractive,
});
@@ -147,12 +158,12 @@ export default class EnvironmentVariableCreate extends EasCommand {
await this.promptForOverwriteAsync({
nonInteractive,
force,
message: `Shared variable with ${name} name already exists on this account.`,
message: `Account-wide variable with ${name} name already exists on this account.`,
suggestion: 'Do you want to unlink it first?',
});
Log.withTick(
`Unlinking shared variable ${chalk.bold(name)} on project ${chalk.bold(
`Unlinking account-wide variable ${chalk.bold(name)} on project ${chalk.bold(
projectDisplayName
)}.`
);
@@ -216,8 +227,8 @@ export default class EnvironmentVariableCreate extends EasCommand {
overwrite = true;
} else {
throw new Error(
`Shared variable with ${name} name already exists on this account.\n` +
`Use a different name or delete the existing variable on website or by using eas env:delete --name ${name} --scope shared command.`
`Account-wide variable with ${name} name already exists on this account.\n` +
`Use a different name or delete the existing variable on website or by using the "eas env:delete --name ${name} --scope account" command.`
);
}
}
@@ -239,6 +250,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
value,
visibility,
environments,
isGlobal: true, // TODO: every account-wide variable is global for now so it's not user facing
type: type ?? EnvironmentSecretType.String,
},
ownerAccount.id
@@ -256,7 +268,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
if (link) {
Log.withTick(
`Linking shared variable ${chalk.bold(name)} to project ${chalk.bold(
`Linking account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
@@ -271,7 +283,9 @@ export default class EnvironmentVariableCreate extends EasCommand {
});
Log.withTick(
`Linked shared variable ${chalk.bold(name)} to project ${chalk.bold(projectDisplayName)}.`
`Linked account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
}
}
@@ -390,7 +404,6 @@ export default class EnvironmentVariableCreate extends EasCommand {
visibility: newVisibility,
link: rest.link ?? false,
force: rest.force ?? false,
scope: rest.scope ?? EnvironmentVariableScope.Project,
'non-interactive': nonInteractive,
type: newType,
fileName,
@@ -398,24 +411,19 @@ export default class EnvironmentVariableCreate extends EasCommand {
};
}
private validateFlags(flags: CreateFlags & { type?: string; visibility?: string }): CreateFlags {
if (flags.scope !== EnvironmentVariableScope.Shared && flags.link) {
private sanitizeFlags(flags: RawCreateFlags): CreateFlags {
if (flags.scope !== 'account' && flags.link) {
throw new Error(
`Unexpected argument: --link can only be used when creating shared variables`
`Unexpected argument: --link can only be used when creating account-wide variables`
);
}
if (
flags.scope === EnvironmentVariableScope.Shared &&
flags.environment &&
!flags.link &&
flags['non-interactive']
) {
throw new Error(
'Unexpected argument: --environment in non-interactive mode can only be used with --link flag.'
);
}
return flags;
return {
...flags,
scope:
flags.scope === 'account'
? EnvironmentVariableScope.Shared
: EnvironmentVariableScope.Project,
};
}
}
+26 -15
View File
@@ -4,8 +4,9 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
@@ -15,17 +16,22 @@ import Log from '../../log';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
type DeleteFlags = {
interface DeleteFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
scope?: EnvironmentVariableScope;
};
scope: EnvironmentVariableScope;
}
export default class EnvironmentVariableDelete extends EasCommand {
static override description = 'delete an environment variable by name';
interface RawDeleteFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
scope: EASEnvironmentVariableScopeFlagValue;
}
static override hidden = true;
export default class EnvDelete extends EasCommand {
static override description = 'delete an environment variable for the current project or account';
static override flags = {
'variable-name': Flags.string({
@@ -35,7 +41,7 @@ export default class EnvironmentVariableDelete extends EasCommand {
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to delete',
}),
...EASVariableScopeFlag,
...EASEnvironmentVariableScopeFlag,
...EASNonInteractiveFlag,
};
@@ -54,17 +60,17 @@ export default class EnvironmentVariableDelete extends EasCommand {
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentVariableDelete);
const { args, flags } = await this.parse(EnvDelete);
const {
'variable-name': name,
'variable-environment': environment,
'non-interactive': nonInteractive,
scope,
} = this.validateInputs(flags, args);
} = this.sanitizeInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariableDelete, {
} = await this.getContextAsync(EnvDelete, {
nonInteractive,
});
@@ -139,8 +145,8 @@ export default class EnvironmentVariableDelete extends EasCommand {
Log.withTick(`️Deleted variable ${selectedVariable.name}".`);
}
private validateInputs(
flags: DeleteFlags,
private sanitizeInputs(
flags: RawDeleteFlags,
{ environment }: { environment?: string }
): DeleteFlags {
if (flags['non-interactive']) {
@@ -153,6 +159,11 @@ export default class EnvironmentVariableDelete extends EasCommand {
}
}
const scope =
flags.scope === 'account'
? EnvironmentVariableScope.Shared
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toUpperCase();
@@ -161,9 +172,9 @@ export default class EnvironmentVariableDelete extends EasCommand {
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return { ...flags, 'variable-environment': environment };
return { ...flags, 'variable-environment': environment, scope };
}
return flags;
return { ...flags, scope };
}
}
+60 -29
View File
@@ -28,9 +28,7 @@ interface RawFlags {
export default class EnvExec extends EasCommand {
static override description =
'execute a bash command with environment variables from the selected environment';
static override hidden = true;
'execute a command with environment variables from the selected environment';
static override contextDefinition = {
...this.ContextOptions.ProjectId,
@@ -55,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 {
@@ -67,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 }));
@@ -74,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(
@@ -105,6 +114,33 @@ export default class EnvExec extends EasCommand {
};
}
private async runCommandNonInteractiveWithEnvVarsAsync({
command,
environmentVariables,
}: {
command: string;
environmentVariables: Record<string, string>;
}): Promise<void> {
await spawnAsync('bash', ['-c', command], {
stdio: 'inherit',
env: {
...process.env,
...environmentVariables,
},
});
}
// 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 runCommandWithEnvVarsAsync({
command,
environmentVariables,
@@ -149,10 +185,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, {
@@ -160,31 +198,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) {
+26 -14
View File
@@ -4,9 +4,10 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
EASNonInteractiveFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import {
@@ -20,18 +21,24 @@ import Log from '../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';
type GetFlags = {
interface RawGetFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
format?: string;
scope: EASEnvironmentVariableScopeFlagValue;
}
interface GetFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
format?: string;
scope: EnvironmentVariableScope;
};
}
export default class EnvironmentVariableGet extends EasCommand {
static override description = 'get environment variable';
static override hidden = true;
export default class EnvGet extends EasCommand {
static override description = 'view an environment variable for the current project or account';
static override contextDefinition = {
...this.ContextOptions.ProjectId,
@@ -56,12 +63,12 @@ export default class EnvironmentVariableGet extends EasCommand {
description: 'Current environment of the variable',
}),
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASEnvironmentVariableScopeFlag,
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentVariableGet);
const { args, flags } = await this.parse(EnvGet);
let {
'variable-environment': environment,
@@ -69,12 +76,12 @@ export default class EnvironmentVariableGet extends EasCommand {
'non-interactive': nonInteractive,
format,
scope,
} = this.validateInputs(flags, args);
} = this.sanitizeInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariableGet, {
} = await this.getContextAsync(EnvGet, {
nonInteractive,
});
@@ -126,7 +133,7 @@ export default class EnvironmentVariableGet extends EasCommand {
}
}
private validateInputs(flags: GetFlags, { environment }: { environment?: string }): GetFlags {
private sanitizeInputs(flags: RawGetFlags, { environment }: { environment?: string }): GetFlags {
if (flags['non-interactive']) {
if (!flags['variable-name']) {
throw new Error('Variable name is required. Run the command with --variable-name flag.');
@@ -144,6 +151,10 @@ export default class EnvironmentVariableGet extends EasCommand {
);
}
const scope =
flags.scope === 'account'
? EnvironmentVariableScope.Shared
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toUpperCase();
if (!isEnvironment(environment)) {
@@ -152,11 +163,12 @@ export default class EnvironmentVariableGet extends EasCommand {
);
}
return {
'variable-environment': environment,
...flags,
'variable-environment': environment,
scope,
};
}
return flags;
return { ...flags, scope };
}
}
+9 -10
View File
@@ -23,9 +23,10 @@ type LinkFlags = {
environment?: EnvironmentVariableEnvironment[];
};
export default class EnvironmentVariableLink extends EasCommand {
static override description = 'link a shared environment variable to the current project';
export default class EnvLink extends EasCommand {
static override description = 'link an account-wide environment variable to the current project';
// for now we only roll out global account-wide env variables so this should stay hidden
static override hidden = true;
static override flags = {
@@ -55,7 +56,7 @@ export default class EnvironmentVariableLink extends EasCommand {
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentVariableLink);
const { args, flags } = await this.parse(EnvLink);
let {
'variable-name': name,
@@ -67,7 +68,7 @@ export default class EnvironmentVariableLink extends EasCommand {
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariableLink, {
} = await this.getContextAsync(EnvLink, {
nonInteractive,
});
@@ -87,7 +88,7 @@ export default class EnvironmentVariableLink extends EasCommand {
);
}
selectedVariable = await selectAsync(
'Select shared variable',
'Select account-wide variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
@@ -96,7 +97,7 @@ export default class EnvironmentVariableLink extends EasCommand {
}
if (!selectedVariable) {
throw new Error(`Shared variable ${name} doesn't exist`);
throw new Error(`Account-wide variable ${name} doesn't exist`);
}
let explicitSelect = false;
@@ -172,7 +173,7 @@ export default class EnvironmentVariableLink extends EasCommand {
}
}
}
private validateInputs(flags: LinkFlags, { environment }: Record<string, string>): LinkFlags {
private validateInputs(flags: LinkFlags, { environment }: { environment?: string }): LinkFlags {
environment = environment?.toUpperCase();
if (environment && !isEnvironment(environment)) {
@@ -196,9 +197,7 @@ export default class EnvironmentVariableLink extends EasCommand {
}
return {
'variable-name': flags['variable-name'],
'variable-environment': flags['variable-environment'],
'non-interactive': flags['non-interactive'],
...flags,
environment: environments,
};
}
+28 -16
View File
@@ -4,9 +4,10 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
EASMultiEnvironmentFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
import {
@@ -66,19 +67,26 @@ async function getVariablesForScopeAsync(
});
}
type ListFlags = {
scope: EnvironmentVariableScope;
interface RawListFlags {
scope: EASEnvironmentVariableScopeFlagValue;
format: string;
environment: EnvironmentVariableEnvironment[] | undefined;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive'?: boolean;
};
}
export default class EnvironmentValueList extends EasCommand {
static override description = 'list environment variables for the current project';
interface ListFlags {
scope: EnvironmentVariableScope;
format: string;
environment: EnvironmentVariableEnvironment[] | undefined;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive': boolean;
}
static override hidden = true;
export default class EnvList extends EasCommand {
static override description = 'list environment variables for the current project or account';
static override contextDefinition = {
...this.ContextOptions.ProjectId,
@@ -96,7 +104,7 @@ export default class EnvironmentValueList extends EasCommand {
}),
...EASMultiEnvironmentFlag,
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASEnvironmentVariableScopeFlag,
};
static override args = [
@@ -109,7 +117,7 @@ export default class EnvironmentValueList extends EasCommand {
];
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentValueList);
const { args, flags } = await this.parse(EnvList);
let {
format,
@@ -118,12 +126,12 @@ export default class EnvironmentValueList extends EasCommand {
'include-sensitive': includeSensitive,
'include-file-content': includeFileContent,
'non-interactive': nonInteractive,
} = this.validateInputs(flags, args);
} = this.sanitizeInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentValueList, {
} = await this.getContextAsync(EnvList, {
nonInteractive: true,
});
@@ -156,7 +164,7 @@ export default class EnvironmentValueList extends EasCommand {
}
} else {
if (scope === EnvironmentVariableScope.Shared) {
Log.log(chalk.bold('Shared variables for this account:'));
Log.log(chalk.bold('Account-wide variables for this account:'));
} else {
Log.log(chalk.bold(`Variables for this project:`));
}
@@ -167,10 +175,10 @@ export default class EnvironmentValueList extends EasCommand {
});
}
private validateInputs(
flags: ListFlags,
{ environment }: Record<string, string>
): ListFlags & { 'non-interactive': boolean } {
private sanitizeInputs(
flags: RawListFlags,
{ environment }: { environment?: string }
): ListFlags {
if (environment && !isEnvironment(environment.toUpperCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
@@ -185,6 +193,10 @@ export default class EnvironmentValueList extends EasCommand {
...flags,
'non-interactive': flags['non-interactive'] ?? false,
environment: environments,
scope:
flags.scope === 'account'
? EnvironmentVariableScope.Shared
: EnvironmentVariableScope.Project,
};
}
}
+12 -13
View File
@@ -15,10 +15,9 @@ import { confirmAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
export default class EnvironmentVariablePull extends EasCommand {
static override description = 'pull env file';
static override hidden = true;
export default class EnvPull extends EasCommand {
static override description =
'pull environment variables for the selected environment to .env file';
static override contextDefinition = {
...this.ContextOptions.ProjectId,
@@ -48,7 +47,7 @@ export default class EnvironmentVariablePull extends EasCommand {
let {
args: { environment: argEnvironment },
flags: { environment: flagEnvironment, path: targetPath, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentVariablePull);
} = await this.parse(EnvPull);
let environment = flagEnvironment?.toUpperCase() ?? argEnvironment?.toUpperCase();
@@ -64,7 +63,7 @@ export default class EnvironmentVariablePull extends EasCommand {
projectId,
loggedIn: { graphqlClient },
projectDir,
} = await this.getContextAsync(EnvironmentVariablePull, {
} = await this.getContextAsync(EnvPull, {
nonInteractive,
});
@@ -117,7 +116,7 @@ export default class EnvironmentVariablePull extends EasCommand {
return `${variable.name}=${currentEnvLocal[variable.name]}`;
}
skippedSecretVariables.push(variable.name);
return `# ${variable.name}=***** (secret variables are not available for reading)`;
return `# ${variable.name}=***** (secret)`;
}
if (variable.type === EnvironmentSecretType.FileBase64 && variable.valueWithFileContent) {
const filePath = path.join(envDir, variable.name);
@@ -131,20 +130,20 @@ export default class EnvironmentVariablePull extends EasCommand {
await fs.writeFile(targetPath, filePrefix + envFileContentLines.join('\n'));
Log.log(
`Pulled environment variables from ${environment.toLowerCase()} environment to ${targetPath}.`
`Pulled plain text and sensitive environment variables from "${environment.toLowerCase()}" environment to ${targetPath}.`
);
if (overridenSecretVariables.length > 0) {
Log.addNewLineIfNone();
Log.log(`Reused local values for following secrets: ${overridenSecretVariables.join('\n')}`);
Log.log(`Reused local values for following secrets: ${overridenSecretVariables.join('\n')}.`);
}
if (skippedSecretVariables.length > 0) {
Log.addNewLineIfNone();
Log.warn(
`The following variables have the encrypted visibility and can not be read outside of EAS servers. Set their values manually in .env.local: ${skippedSecretVariables.join(
'\n'
)}`
Log.log(
`The following variables have the secret visibility and can't be read outside of EAS servers. Set their values manually in your .env file: ${skippedSecretVariables.join(
', '
)}.`
);
}
}
+7 -8
View File
@@ -21,10 +21,9 @@ import { confirmAsync, promptAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
export default class EnvironmentVariablePush extends EasCommand {
static override description = 'push env file';
static override hidden = true;
export default class EnvPush extends EasCommand {
static override description =
'push environment variables from .env file to the selected environment';
static override contextDefinition = {
...this.ContextOptions.ProjectId,
@@ -49,14 +48,14 @@ export default class EnvironmentVariablePush extends EasCommand {
];
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentVariablePush);
const { args, flags } = await this.parse(EnvPush);
let { environment: environments, path: envPath } = this.parseFlagsAndArgs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariablePush, {
} = await this.getContextAsync(EnvPush, {
nonInteractive: false,
});
@@ -106,12 +105,12 @@ export default class EnvironmentVariablePush extends EasCommand {
const existingDifferentSharedVariablesNames = existingDifferentSharedVariables.map(
variable => variable.name
);
Log.error('Shared variables cannot be overwritten by eas env:push command.');
Log.error('Account-wide variables cannot be overwritten by eas env:push command.');
Log.error('Remove them from the env file or unlink them from the project to continue:');
existingDifferentSharedVariablesNames.forEach(name => {
Log.error(`- ${name}`);
});
throw new Error('Shared variables cannot be overwritten by eas env:push command');
throw new Error('Account-wide variables cannot be overwritten by eas env:push command');
}
if (existingDifferentVariables.length > 0) {
+8 -6
View File
@@ -18,9 +18,11 @@ type UnlinkFlags = {
environment?: EnvironmentVariableEnvironment[];
};
export default class EnvironmentVariableUnlink extends EasCommand {
static override description = 'unlink a shared environment variable to the current project';
export default class EnvUnlink extends EasCommand {
static override description =
'unlink an account-wide environment variable from the current project';
// for now we only roll out global account-wide env variables so this should stay hidden
static override hidden = true;
static override flags = {
@@ -46,7 +48,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
];
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentVariableUnlink);
const { args, flags } = await this.parse(EnvUnlink);
let {
'variable-name': name,
@@ -57,7 +59,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariableUnlink, {
} = await this.getContextAsync(EnvUnlink, {
nonInteractive,
});
@@ -74,7 +76,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
throw new Error("Multiple variables found, please select one using '--variable-name'");
}
selectedVariable = await selectAsync(
'Select shared variable',
'Select account-wide variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
@@ -83,7 +85,7 @@ export default class EnvironmentVariableUnlink extends EasCommand {
}
if (!selectedVariable) {
throw new Error(`Shared variable ${name} doesn't exist`);
throw new Error(`Account-wide variable ${name} doesn't exist`);
}
let explicitSelect = false;
+35 -19
View File
@@ -6,9 +6,10 @@ import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EASVariableVisibilityFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
@@ -37,23 +38,32 @@ import {
} from '../../utils/prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
type UpdateFlags = {
interface RawUpdateFlags {
name?: string;
value?: string;
scope?: EnvironmentVariableScope;
scope: EASEnvironmentVariableScopeFlagValue;
environment?: EnvironmentVariableEnvironment[];
visibility?: 'plaintext' | 'sensitive' | 'encrypted';
visibility?: 'plaintext' | 'sensitive' | 'secret';
type?: 'string' | 'file';
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
};
}
export default class EnvironmentVariableUpdate extends EasCommand {
static override description =
'update an environment variable on the current project or owner account';
interface UpdateFlags {
name?: string;
value?: string;
scope: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment[];
visibility?: 'plaintext' | 'sensitive' | 'secret';
type?: 'string' | 'file';
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
}
static override hidden = true;
export default class EnvUpdate extends EasCommand {
static override description = 'update an environment variable on the current project or account';
static override flags = {
'variable-name': Flags.string({
@@ -74,7 +84,7 @@ export default class EnvironmentVariableUpdate extends EasCommand {
options: ['string', 'file'],
}),
...EASVariableVisibilityFlag,
...EASVariableScopeFlag,
...EASEnvironmentVariableScopeFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -95,7 +105,7 @@ export default class EnvironmentVariableUpdate extends EasCommand {
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentVariableUpdate);
const { args, flags } = await this.parse(EnvUpdate);
const {
name,
value: rawValue,
@@ -106,12 +116,12 @@ export default class EnvironmentVariableUpdate extends EasCommand {
environment: environments,
type,
visibility,
} = this.validateFlags(flags, args);
} = this.sanitizeInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentVariableUpdate, {
} = await this.getContextAsync(EnvUpdate, {
nonInteractive,
});
@@ -177,6 +187,7 @@ export default class EnvironmentVariableUpdate extends EasCommand {
visibility,
'non-interactive': nonInteractive,
type,
scope,
});
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
@@ -194,8 +205,8 @@ export default class EnvironmentVariableUpdate extends EasCommand {
Log.withTick(`Updated variable ${chalk.bold(selectedVariable.name)} ${suffix}.`);
}
private validateFlags(
flags: UpdateFlags,
private sanitizeInputs(
flags: RawUpdateFlags,
{ environment }: { environment?: string }
): UpdateFlags {
if (flags['non-interactive']) {
@@ -209,6 +220,11 @@ export default class EnvironmentVariableUpdate extends EasCommand {
}
}
const scope =
flags.scope === 'account'
? EnvironmentVariableScope.Shared
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toUpperCase();
if (!isEnvironment(environment)) {
@@ -217,12 +233,13 @@ export default class EnvironmentVariableUpdate extends EasCommand {
);
}
return {
'variable-environment': environment,
...flags,
'variable-environment': environment,
scope,
};
}
return flags;
return { ...flags, scope };
}
private async promptForMissingFlagsAsync(
@@ -328,15 +345,14 @@ export default class EnvironmentVariableUpdate extends EasCommand {
}
return {
...rest,
name,
value,
environment: environments,
visibility: newVisibility,
scope: rest.scope ?? EnvironmentVariableScope.Project,
'non-interactive': nonInteractive,
type: newType,
fileName,
...rest,
};
}
}
@@ -0,0 +1,378 @@
import { Platform } from '@expo/eas-build-job';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { AppPlatform, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log 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 formatFields, { FormatFieldsItem } from '../../utils/formatFields';
import { enableJsonOutput } from '../../utils/json';
export default class FingerprintCompare extends EasCommand {
static override description = 'compare fingerprints of the current project, builds and updates';
static override hidden = true;
static override flags = {
'build-id': Flags.string({
aliases: ['buildId'],
description: 'Compare the fingerprint with the build with the specified ID',
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(FingerprintCompare);
const { json: jsonFlag, 'non-interactive': nonInteractive, buildId: buildIdFromArg } = flags;
const {
projectId,
privateProjectConfig: { projectDir },
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(FingerprintCompare, {
nonInteractive,
withServerSideEnvironment: null,
});
if (jsonFlag) {
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');
}
buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, {
filters: { hasFingerprint: true },
});
if (!buildId) {
return;
}
}
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.');
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`);
return;
} else {
Log.log(`🔄 Project fingerprint differs from build`);
}
const fingerprintDiffs = diffFingerprint(projectDir, fingerprint, projectFingerprint);
if (!fingerprintDiffs) {
Log.error('Fingerprint diffs can only be computed for projects with SDK 52 or higher');
return;
}
const filePathDiffs = fingerprintDiffs.filter(diff => {
let sourceType;
if (diff.op === 'added') {
sourceType = diff.addedSource.type;
} else if (diff.op === 'removed') {
sourceType = diff.removedSource.type;
} else if (diff.op === 'changed') {
sourceType = diff.beforeSource.type;
}
return sourceType === 'dir' || sourceType === 'file';
});
if (filePathDiffs.length > 0) {
Log.newLine();
Log.log('📁 Paths with native dependencies:');
}
const fields = [];
for (const diff of filePathDiffs) {
const field = getDiffFilePathFields(diff);
if (!field) {
throw new Error(`Unsupported diff: ${JSON.stringify(diff)}`);
}
fields.push(field);
}
Log.log(
formatFields(fields, {
labelFormat: label => ` ${chalk.dim(label)}:`,
})
);
const contentDiffs = fingerprintDiffs.filter(diff => {
let sourceType;
if (diff.op === 'added') {
sourceType = diff.addedSource.type;
} else if (diff.op === 'removed') {
sourceType = diff.removedSource.type;
} else if (diff.op === 'changed') {
sourceType = diff.beforeSource.type;
}
return sourceType === 'contents';
});
for (const diff of contentDiffs) {
printContentDiff(diff);
}
}
}
function printContentDiff(diff: FingerprintDiffItem): void {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
if (sourceType === 'contents') {
printContentSource({
op: diff.op,
sourceType,
contentsId: diff.addedSource.id,
contentsAfter: diff.addedSource.contents,
});
}
} else if (diff.op === 'removed') {
const sourceType = diff.removedSource.type;
if (sourceType === 'contents') {
printContentSource({
op: diff.op,
sourceType,
contentsId: diff.removedSource.id,
contentsBefore: diff.removedSource.contents,
});
}
} else if (diff.op === 'changed') {
const sourceType = diff.beforeSource.type;
if (sourceType === 'contents') {
if (diff.afterSource.type !== 'contents') {
throw new Error(
`Changed fingerprint source types must be the same, received ${diff.beforeSource.type}, ${diff.afterSource.type}`
);
}
printContentSource({
op: diff.op,
sourceType: diff.beforeSource.type, // before and after source types should be the same
contentsId: diff.beforeSource.id, // before and after content ids should be the same
contentsBefore: diff.beforeSource.contents,
contentsAfter: diff.afterSource.contents,
});
}
}
}
function getDiffFilePathFields(diff: FingerprintDiffItem): FormatFieldsItem | null {
if (diff.op === 'added') {
const sourceType = diff.addedSource.type;
if (sourceType !== 'contents') {
return getFilePathSourceFields({
op: diff.op,
sourceType,
filePath: diff.addedSource.filePath,
});
}
} else if (diff.op === 'removed') {
const sourceType = diff.removedSource.type;
if (sourceType !== 'contents') {
return getFilePathSourceFields({
op: diff.op,
sourceType,
filePath: diff.removedSource.filePath,
});
}
} else if (diff.op === 'changed') {
const sourceType = diff.beforeSource.type;
if (sourceType !== 'contents') {
return getFilePathSourceFields({
op: diff.op,
sourceType: diff.beforeSource.type, // before and after source types should be the same
filePath: diff.beforeSource.filePath, // before and after filePaths should be the same
});
}
}
return null;
}
function getFilePathSourceFields({
op,
sourceType,
filePath,
}: {
op: 'added' | 'removed' | 'changed';
sourceType: 'dir' | 'file';
filePath: string;
}): FormatFieldsItem {
if (sourceType === 'dir') {
if (op === 'added') {
return { label: 'new directory', value: filePath };
} else if (op === 'removed') {
return { label: 'removed directory', value: filePath };
} else if (op === 'changed') {
return { label: 'modified directory', value: filePath };
}
} else if (sourceType === 'file') {
if (op === 'added') {
return { label: 'new file', value: filePath };
} else if (op === 'removed') {
return { label: 'removed file', value: filePath };
} else if (op === 'changed') {
return { label: 'modified file', value: filePath };
}
}
throw new Error(`Unsupported source and op: ${sourceType}, ${op}`);
}
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',
};
function printContentSource({
op,
contentsBefore,
contentsAfter,
contentsId,
}: {
op: 'added' | 'removed' | 'changed';
sourceType: 'contents';
contentsBefore?: string | Buffer;
contentsAfter?: string | Buffer;
contentsId: string;
}): void {
Log.newLine();
const prettyContentId = PRETTY_CONTENT_ID[contentsId] ?? contentsId;
if (op === 'added') {
Log.log(`${chalk.dim('📝 New content')}: ${prettyContentId}`);
} else if (op === 'removed') {
Log.log(`${chalk.dim('📝 Removed content')}: ${prettyContentId}`);
} else if (op === 'changed') {
Log.log(`${chalk.dim('📝 Modified content')}: ${prettyContentId}`);
}
printContentsDiff(contentsBefore ?? '', contentsAfter ?? '');
}
function printContentsDiff(contents1: string | Buffer, contents2: string | Buffer): void {
const stringifiedContents1 = Buffer.isBuffer(contents1) ? contents1.toString() : contents1;
const stringifiedContents2 = Buffer.isBuffer(contents2) ? contents2.toString() : contents2;
const isStr1JSON = isJSON(stringifiedContents1);
const isStr2JSON = isJSON(stringifiedContents2);
const prettifiedContents1 = isStr1JSON
? JSON.stringify(JSON.parse(stringifiedContents1), null, 2)
: stringifiedContents1;
const prettifiedContents2 = isStr2JSON
? JSON.stringify(JSON.parse(stringifiedContents2), null, 2)
: stringifiedContents2;
abridgedDiff(prettifiedContents1, prettifiedContents2, 0);
}
function isJSON(str: string): boolean {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
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(
graphqlClient: ExpoGraphqlClient,
projectId: string,
projectDisplayName: string,
{
filters,
}: {
filters?: {
statuses?: BuildStatus[];
platform?: RequestedPlatform;
profile?: string;
hasFingerprint?: boolean;
};
} = {}
): Promise<string | null> {
const spinner = ora().start('Fetching builds…');
let builds;
try {
builds = await fetchBuildsAsync({ graphqlClient, projectId, filters });
spinner.stop();
} catch (error) {
spinner.fail(
`Something went wrong and we couldn't fetch the builds for the project ${projectDisplayName}.`
);
throw error;
}
if (builds.length === 0) {
Log.warn(`No fingerprints have been computed for builds of project ${projectDisplayName}.`);
return null;
} else {
const build = await selectAsync<string>(
'Which build do you want to compare?',
builds.map(build => ({
title: formatBuild(build),
value: build.id,
}))
);
return build;
}
}
@@ -8,14 +8,13 @@ import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { AppPrivacy, Role } from '../../graphql/generated';
import { Role } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../project/projectUtils';
import { Choice, confirmAsync, promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
@@ -337,7 +336,6 @@ export default class ProjectInit extends EasCommand {
createdProjectId = await AppMutation.createAppAsync(graphqlClient, {
accountId: account.id,
projectName,
privacy: toAppPrivacy(exp.privacy) ?? AppPrivacy.Public,
});
spinner.succeed(`Created ${chalk.bold(projectLink)}`);
} catch (err) {
@@ -25,6 +25,7 @@ import { promptAsync, selectAsync } from '../../prompts';
export default class EnvironmentSecretCreate extends EasCommand {
static override description =
'create an environment secret on the current project or owner account';
static override hidden = true;
static override flags = {
scope: Flags.enum({
@@ -55,6 +56,9 @@ export default class EnvironmentSecretCreate extends EasCommand {
};
async runAsync(): Promise<void> {
Log.warn('This command is deprecated. Use eas env:create instead.');
Log.newLine();
let {
flags: {
name,
@@ -13,6 +13,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static override description = 'delete an environment secret by ID';
static override hidden = true;
static override flags = {
id: Flags.string({
@@ -27,6 +28,9 @@ export default class EnvironmentSecretDelete extends EasCommand {
};
async runAsync(): Promise<void> {
Log.warn('This command is deprecated. Use eas env:delete instead.');
Log.newLine();
let {
flags: { id, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentSecretDelete);
@@ -11,6 +11,7 @@ import formatFields from '../../utils/formatFields';
export default class EnvironmentSecretList extends EasCommand {
static override description = 'list environment secrets available for your current app';
static override hidden = true;
static override contextDefinition = {
...this.ContextOptions.ProjectId,
@@ -18,6 +19,9 @@ export default class EnvironmentSecretList extends EasCommand {
};
async runAsync(): Promise<void> {
Log.warn('This command is deprecated. Use eas env:list instead.');
Log.newLine();
const {
projectId,
loggedIn: { graphqlClient },
@@ -24,6 +24,7 @@ import intersection from '../../utils/expodash/intersection';
export default class EnvironmentSecretPush extends EasCommand {
static override description = 'read environment secrets from env file and store on the server';
static override hidden = true;
static override flags = {
scope: Flags.enum({
@@ -47,6 +48,9 @@ export default class EnvironmentSecretPush extends EasCommand {
};
async runAsync(): Promise<void> {
Log.warn('This command is deprecated. Use eas env:push instead.');
Log.newLine();
const {
flags: { scope, force, 'env-file': maybeEnvFilePath, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentSecretPush);
@@ -88,7 +88,10 @@ describe(UpdatePublish.name, () => {
const { platforms, runtimeVersion } = mockTestExport();
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
branch: {
id: 'branch123',
name: 'wat',
},
createdBranch: false,
});
@@ -111,7 +114,10 @@ describe(UpdatePublish.name, () => {
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: 'branch123', branchName: 'branchFromChannel' });
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
branch: {
id: 'branch123',
name: 'wat',
},
createdBranch: false,
});
@@ -146,7 +152,10 @@ describe(UpdatePublish.name, () => {
// Mock an existing branch, so we don't create a new one
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
branch: {
id: 'branch123',
name: 'wat',
},
createdBranch: false,
});
@@ -1,5 +1,4 @@
import { AppJSONConfig, ExpoConfig, PackageJSONConfig, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import nullthrows from 'nullthrows';
import path from 'path';
@@ -82,16 +81,23 @@ describe(UpdateRollBackToEmbedded.name, () => {
);
});
it('creates a roll back to embedded with --non-interactive, --branch, and --message', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
it('creates a roll back to embedded with --non-interactive, --branch, --message, and --runtime-version', async () => {
const flags = [
'--non-interactive',
'--branch=branch123',
'--message=abc',
'--runtime-version=exposdk:47.0.0',
];
mockTestProject();
const platforms = ['android', 'ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
branch: {
id: 'branch123',
name: 'wat',
},
createdBranch: false,
});
@@ -104,19 +110,26 @@ describe(UpdateRollBackToEmbedded.name, () => {
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a roll back to embedded with --non-interactive, --channel, and --message', async () => {
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
it('creates a roll back to embedded with --non-interactive, --channel, --message, and --runtime-version', async () => {
const flags = [
'--non-interactive',
'--channel=channel123',
'--message=abc',
'--runtime-version=exposdk:47.0.0',
];
const { projectId } = mockTestProject();
const platforms = ['android', 'ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
jest
.mocked(getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)
.mockResolvedValue({ branchId: updateStub.branch.id, branchName: 'branchFromChannel' });
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
branch: {
id: 'branch123',
name: 'wat',
},
createdBranch: false,
});
@@ -142,18 +155,25 @@ describe(UpdateRollBackToEmbedded.name, () => {
});
it('creates a roll back to embedded with the public expo config', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
const flags = [
'--non-interactive',
'--branch=branch123',
'--message=abc',
'--runtime-version=exposdk:47.0.0',
];
// Add configuration to the project that should not be included in the update
mockTestProject();
const platforms = ['ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersionAsync).mockResolvedValue(runtimeVersion);
// Mock an existing branch, so we don't create a new one
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
branch: {
id: 'branch123',
name: 'wat',
},
createdBranch: false,
});
+46 -19
View File
@@ -13,6 +13,7 @@ import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
EnvironmentVariableEnvironment,
FingerprintInfoGroup,
PublishUpdateGroupInput,
StatuspageServiceName,
UpdateInfoGroup,
@@ -39,6 +40,7 @@ import {
getRuntimeVersionInfoObjectsAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync,
platformDisplayNames,
resolveInputDirectoryAsync,
uploadAssetsAsync,
@@ -398,27 +400,38 @@ export default class UpdatePublish extends EasCommand {
runtimeVersionInfoObjects
);
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
const { branch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping = await Promise.all(
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
return {
...info,
fingerprintSource: info.fingerprint
? (
await maybeUploadFingerprintAsync({
hash: info.runtimeVersion,
fingerprint: info.fingerprint,
graphqlClient,
})
).fingerprintSource ?? null
: null,
};
})
);
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMappingFromExpoUpdates =
await Promise.all(
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
return {
...info,
fingerprintSource: info.fingerprint
? (
await maybeUploadFingerprintAsync({
hash: info.runtimeVersion,
fingerprint: info.fingerprint,
graphqlClient,
})
).fingerprintSource ?? null
: null,
};
})
);
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping =
await maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync({
projectDir,
graphqlClient,
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping:
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMappingFromExpoUpdates,
workflowsByPlatform: workflows,
env: undefined,
});
const runtimeVersionToRolloutInfoGroup =
rolloutPercentage !== undefined
@@ -436,7 +449,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 }) => {
({ runtimeVersion, platforms, fingerprintSource, fingerprintInfoGroup }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
@@ -455,19 +468,33 @@ export default class UpdatePublish extends EasCommand {
])
)
: null;
const transformedFingerprintInfoGroup = Object.entries(fingerprintInfoGroup).reduce(
(prev, [platform, fingerprintInfo]) => {
return {
...prev,
[platform]: {
...fingerprintInfo,
fingerprintSource: transformFingerprintSource(fingerprintInfo.fingerprintSource),
},
};
},
{} as FingerprintInfoGroup
);
return {
branchId,
branchId: branch.id,
updateInfoGroup: localUpdateInfoGroup,
rolloutInfoGroup: localRolloutInfoGroup,
runtimeFingerprintSource: fingerprintSource
? transformFingerprintSource(fingerprintSource)
: null,
fingerprintInfoGroup: transformedFingerprintInfoGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
environment: environment ?? null,
};
}
);
@@ -1,5 +1,4 @@
import { Platform as PublishPlatform } from '@expo/config';
import { Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import nullthrows from 'nullthrows';
@@ -12,10 +11,12 @@ import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
PublishUpdateGroupInput,
RuntimeFragment,
StatuspageServiceName,
UpdatePublishMutation,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { RuntimeQuery } from '../../graphql/queries/RuntimeQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
@@ -29,10 +30,8 @@ import {
defaultPublishPlatforms,
getBranchNameForCommandAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
getRuntimeVersionInfoObjectsAsync,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
import {
@@ -45,12 +44,13 @@ import {
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { Connection, QueryParams, selectPaginatedAsync } from '../../utils/relay';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
type RawUpdateFlags = {
auto: boolean;
branch?: string;
channel?: string;
'runtime-version'?: string;
message?: string;
platform: string;
'private-key-path'?: string;
@@ -59,10 +59,10 @@ type RawUpdateFlags = {
};
type UpdateFlags = {
auto: boolean;
platform: RequestedPlatform;
branchName?: string;
channelName?: string;
runtimeVersion?: string;
updateMessage?: string;
privateKeyPath?: string;
json: boolean;
@@ -81,6 +81,10 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
description: 'Channel that the published rollback to embedded update should affect',
required: false,
}),
'runtime-version': Flags.string({
description: 'Runtime version that the rollback to embedded update should target',
required: false,
}),
message: Flags.string({
description: 'A short message describing the rollback to embedded update',
required: false,
@@ -95,11 +99,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
default: 'all',
required: false,
}),
auto: Flags.boolean({
description:
'Use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory. Only relevant if you are using code signing: https://docs.expo.dev/eas-update/code-signing/`,
required: false,
@@ -117,10 +116,10 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const { flags: rawFlags } = await this.parse(UpdateRollBackToEmbedded);
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
const {
auto: autoFlag,
platform: platformFlag,
channelName: channelNameArg,
updateMessage: updateMessageArg,
runtimeVersion: runtimeVersionArg,
privateKeyPath,
json: jsonFlag,
nonInteractive,
@@ -171,14 +170,14 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
projectId,
channelNameArg,
branchNameArg,
autoFlag,
autoFlag: false,
nonInteractive,
paginatedQueryOptions,
});
const updateMessage = await getUpdateMessageForCommandAsync(vcsClient, {
updateMessageArg,
autoFlag,
autoFlag: false,
nonInteractive,
jsonFlag,
});
@@ -186,28 +185,33 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const realizedPlatforms: UpdatePublishPlatform[] =
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
const { branch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const selectedRuntime =
runtimeVersionArg ??
(
await UpdateRollBackToEmbedded.selectRuntimeAsync(graphqlClient, {
appId: projectId,
branchName,
})
)?.version;
if (!selectedRuntime) {
Errors.error('Must select a runtime or provide the --runtimeVersion flag', { exit: 1 });
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
const runtimeVersionInfoObjects = await getRuntimeVersionInfoObjectsAsync({
exp,
platforms: realizedPlatforms,
projectDir,
workflows: {
...workflows,
web: Workflow.UNKNOWN,
},
env: undefined,
});
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
runtimeVersionInfoObjects
realizedPlatforms.map(platform => ({
platform,
runtimeVersionInfo: {
runtimeVersion: selectedRuntime,
fingerprint: null,
fingerprintHash: null,
},
}))
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
@@ -215,10 +219,8 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
try {
newUpdates = await this.publishRollbacksAsync({
graphqlClient,
isGitWorkingTreeDirty,
gitCommitHash,
updateMessage,
branchId,
branchId: branch.id,
codeSigningInfo,
runtimeToPlatformsAndFingerprintInfoMapping,
realizedPlatforms,
@@ -232,13 +234,6 @@ export default class UpdateRollBackToEmbedded 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.'
);
}
Log.addNewLineIfNone();
for (const runtime of uniqBy(
@@ -276,14 +271,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: updateMessage ?? '' },
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
@@ -294,8 +281,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
private async publishRollbacksAsync({
graphqlClient,
isGitWorkingTreeDirty,
gitCommitHash,
updateMessage,
branchId,
codeSigningInfo,
@@ -303,8 +288,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
realizedPlatforms,
}: {
graphqlClient: ExpoGraphqlClient;
isGitWorkingTreeDirty: boolean | undefined;
gitCommitHash: string | undefined;
updateMessage: string | undefined;
branchId: string;
codeSigningInfo: CodeSigningInfo | undefined;
@@ -329,8 +312,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
@@ -381,22 +362,63 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
return newUpdates;
}
private static async selectRuntimeAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchName,
batchSize = 5,
}: {
appId: string;
branchName: string;
batchSize?: number;
}
): Promise<RuntimeFragment | null> {
const queryAsync = async (queryParams: QueryParams): Promise<Connection<RuntimeFragment>> => {
return await RuntimeQuery.getRuntimesOnBranchAsync(graphqlClient, {
appId,
name: branchName,
first: queryParams.first,
after: queryParams.after,
last: queryParams.last,
before: queryParams.before,
});
};
const getTitleAsync = async (runtime: RuntimeFragment): Promise<string> => {
return runtime.version;
};
return await selectPaginatedAsync({
queryAsync,
getTitleAsync,
printedType: 'target runtime',
pageSize: batchSize,
});
}
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
const {
branch: branchName,
channel: channelName,
message: updateMessage,
'runtime-version': runtimeVersion,
} = flags;
if (nonInteractive && !(updateMessage && (branchName || channelName))) {
Errors.error(
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
'--branch and --message, or --channel and --message are required in non-interactive mode',
{ exit: 1 }
);
}
if (nonInteractive && !runtimeVersion) {
Errors.error('--runtimeVersion is required in non-interactive mode', { exit: 1 });
}
return {
auto,
branchName,
channelName,
updateMessage,
runtimeVersion,
platform: flags.platform as RequestedPlatform,
privateKeyPath: flags['private-key-path'],
nonInteractive,
@@ -35,9 +35,6 @@ 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 flags = {
@@ -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 beta and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
+34 -14
View File
@@ -43,16 +43,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 {
@@ -66,9 +68,6 @@ export default class WorkerDeploy extends EasCommand {
static override description = 'Deploy your Expo web build';
static override aliases = ['deploy'];
static override usage = [chalk`deploy {dim [options]}`, `deploy --prod`];
// TODO(@kitten): Keep command hidden until worker deployments are live
static override hidden = true;
static override state = 'beta';
static override flags = {
@@ -90,10 +89,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 +111,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 beta and subject to changes.');
const {
getDynamicPrivateProjectConfigAsync,
@@ -250,7 +250,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 +258,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,
@@ -382,6 +399,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 +430,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 +455,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}}`);
}
+12 -3
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,
},
},
}
@@ -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);
}
@@ -32,6 +32,7 @@ export class CredentialsContext {
public readonly analytics: Analytics;
public readonly vcsClient: Client;
public readonly easJsonCliConfig?: EasJson['cli'];
public readonly usesBroadcastPushNotifications: boolean;
private shouldAskAuthenticateAppStore: boolean = true;
@@ -61,6 +62,8 @@ export class CredentialsContext {
this.nonInteractive = options.nonInteractive ?? false;
this.projectInfo = options.projectInfo;
this.freezeCredentials = options.freezeCredentials ?? false;
this.usesBroadcastPushNotifications =
options.projectInfo?.exp.ios?.usesBroadcastPushNotifications ?? false;
}
get hasProjectContext(): boolean {
@@ -35,7 +35,7 @@ export class SetUpTargetBuildCredentials {
bundleIdentifier: app.bundleIdentifier,
projectName: app.projectName,
},
{ entitlements }
{ entitlements, usesBroadcastPushNotifications: ctx.usesBroadcastPushNotifications }
);
}
try {
@@ -45,7 +45,9 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
id: 'XXX',
} as any;
await syncCapabilitiesForEntitlementsAsync(bundleId, entitlements);
await syncCapabilitiesForEntitlementsAsync(bundleId, entitlements, {
usesBroadcastPushNotifications: false,
});
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{
@@ -62,63 +64,70 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(bundleId, {
'com.apple.developer.healthkit': true,
// 'com.apple.developer.healthkit.access': [],
'com.apple.developer.in-app-payments': ['merchant.com.example.development'],
'com.apple.developer.icloud-container-identifiers': [],
'com.apple.developer.ClassKit-environment': 'development',
'com.apple.developer.default-data-protection':
'NSFileProtectionCompleteUntilFirstUserAuthentication',
'com.apple.developer.networking.networkextension': ['dns-proxy'],
'com.apple.developer.networking.vpn.api': ['allow-vpn'],
'com.apple.developer.networking.HotspotConfiguration': true,
'com.apple.developer.kernel.extended-virtual-addressing': true,
'com.apple.developer.homekit': true,
'com.apple.developer.networking.multipath': true,
'com.apple.external-accessory.wireless-configuration': true,
'inter-app-audio': true,
'com.apple.developer.pass-type-identifiers': ['$(TeamIdentifierPrefix)*'],
'com.apple.developer.user-fonts': ['app-usage'],
'com.apple.developer.devicecheck.appattest-environment': 'development',
'com.apple.developer.nfc.readersession.formats': ['NDEF', 'TAG'],
'com.apple.developer.applesignin': ['Default'],
'com.apple.developer.siri': true,
'com.apple.developer.networking.wifi-info': true,
'com.apple.developer.usernotifications.communication': true,
'com.apple.developer.usernotifications.time-sensitive': true,
'com.apple.developer.group-session': true,
'com.apple.developer.family-controls': true,
'com.apple.developer.authentication-services.autofill-credential-provider': true,
// 'com.apple.developer.game-center': true,
'com.apple.security.application-groups': [
'group.CY-A5149AC2-49FC-11E7-B3F3-0335A16FFB8D.com.cydia.Extender',
],
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'com.apple.developer.healthkit': true,
// 'com.apple.developer.healthkit.access': [],
'com.apple.developer.in-app-payments': ['merchant.com.example.development'],
'com.apple.developer.icloud-container-identifiers': [],
'com.apple.developer.ClassKit-environment': 'development',
'com.apple.developer.default-data-protection':
'NSFileProtectionCompleteUntilFirstUserAuthentication',
'com.apple.developer.networking.networkextension': ['dns-proxy'],
'com.apple.developer.networking.vpn.api': ['allow-vpn'],
'com.apple.developer.networking.HotspotConfiguration': true,
'com.apple.developer.kernel.extended-virtual-addressing': true,
'com.apple.developer.homekit': true,
'com.apple.developer.networking.multipath': true,
'com.apple.external-accessory.wireless-configuration': true,
'inter-app-audio': true,
'com.apple.developer.pass-type-identifiers': ['$(TeamIdentifierPrefix)*'],
'com.apple.developer.user-fonts': ['app-usage'],
'com.apple.developer.devicecheck.appattest-environment': 'development',
'com.apple.developer.nfc.readersession.formats': ['NDEF', 'TAG'],
'com.apple.developer.applesignin': ['Default'],
'com.apple.developer.siri': true,
'com.apple.developer.networking.wifi-info': true,
'com.apple.developer.usernotifications.communication': true,
'com.apple.developer.usernotifications.time-sensitive': true,
'com.apple.developer.group-session': true,
'com.apple.developer.family-controls': true,
'com.apple.developer.authentication-services.autofill-credential-provider': true,
// 'com.apple.developer.game-center': true,
'com.apple.security.application-groups': [
'group.CY-A5149AC2-49FC-11E7-B3F3-0335A16FFB8D.com.cydia.Extender',
],
'com.apple.developer.fileprovider.testing-mode': true,
'com.apple.developer.healthkit.recalibrate-estimates': true,
'com.apple.developer.maps': true,
'com.apple.developer.user-management': true,
'com.apple.developer.networking.custom-protocol': true,
'com.apple.developer.system-extension.install': true,
'com.apple.developer.push-to-talk': true,
'com.apple.developer.driverkit.transport.usb': true,
'com.apple.developer.kernel.increased-memory-limit': true,
'com.apple.developer.driverkit.communicates-with-drivers': true,
'com.apple.developer.media-device-discovery-extension': true,
'com.apple.developer.driverkit.allow-third-party-userclients': true,
'com.apple.developer.weatherkit': true,
'com.apple.developer.on-demand-install-capable': true,
'com.apple.developer.driverkit.family.scsicontroller': true,
'com.apple.developer.driverkit.family.serial': true,
'com.apple.developer.driverkit.family.networking': true,
'com.apple.developer.driverkit.family.hid.eventservice': true,
'com.apple.developer.driverkit.family.hid.device': true,
'com.apple.developer.driverkit': true,
'com.apple.developer.driverkit.transport.hid': true,
'com.apple.developer.driverkit.family.audio': true,
'com.apple.developer.shared-with-you': true,
});
'com.apple.developer.fileprovider.testing-mode': true,
'com.apple.developer.healthkit.recalibrate-estimates': true,
'com.apple.developer.maps': true,
'com.apple.developer.user-management': true,
'com.apple.developer.networking.custom-protocol': true,
'com.apple.developer.system-extension.install': true,
'com.apple.developer.push-to-talk': true,
'com.apple.developer.driverkit.transport.usb': true,
'com.apple.developer.kernel.increased-memory-limit': true,
'com.apple.developer.driverkit.communicates-with-drivers': true,
'com.apple.developer.media-device-discovery-extension': true,
'com.apple.developer.driverkit.allow-third-party-userclients': true,
'com.apple.developer.weatherkit': true,
'com.apple.developer.on-demand-install-capable': true,
'com.apple.developer.driverkit.family.scsicontroller': true,
'com.apple.developer.driverkit.family.serial': true,
'com.apple.developer.driverkit.family.networking': true,
'com.apple.developer.driverkit.family.hid.eventservice': true,
'com.apple.developer.driverkit.family.hid.device': true,
'com.apple.developer.driverkit': true,
'com.apple.developer.driverkit.transport.hid': true,
'com.apple.developer.driverkit.family.audio': true,
'com.apple.developer.shared-with-you': true,
'aps-environment': 'production',
},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'HEALTHKIT', option: 'ON' },
@@ -251,6 +260,10 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
capabilityType: 'SHARED_WITH_YOU',
option: 'ON',
},
{
capabilityType: 'PUSH_NOTIFICATIONS',
option: 'ON',
},
]);
expect(results.enabled).toMatchInlineSnapshot(`
@@ -304,6 +317,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
"DriverKit Transport HID (development)",
"DriverKit Family Audio (development)",
"Shared with You",
"Push Notifications",
]
`);
expect(results.disabled).toStrictEqual([]);
@@ -318,9 +332,15 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(bundleId, {
'com.apple.developer.healthkit': true,
});
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'com.apple.developer.healthkit': true,
},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
@@ -344,9 +364,15 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
} as any;
await expect(
syncCapabilitiesForEntitlementsAsync(bundleId, {
'com.apple.developer.healthkit': true,
})
syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'com.apple.developer.healthkit': true,
},
{
usesBroadcastPushNotifications: false,
}
)
).rejects.toThrowError(
`https://developer-mdn.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
);
@@ -361,9 +387,15 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(bundleId, {
'com.apple.developer.healthkit': true,
});
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'com.apple.developer.healthkit': true,
},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'HEALTHKIT', option: 'ON' },
@@ -382,9 +414,15 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(bundleId, {
'com.apple.developer.healthkit': true,
});
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'com.apple.developer.healthkit': true,
},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'HEALTHKIT', option: 'ON' },
@@ -406,7 +444,13 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(bundleId, {});
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
@@ -423,9 +467,15 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(bundleId, {
'com.apple.developer.healthkit': true,
});
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'com.apple.developer.healthkit': true,
},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'HEALTHKIT', option: 'ON' },
@@ -434,4 +484,164 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
expect(results.enabled).toStrictEqual(['HealthKit']);
expect(results.disabled).toStrictEqual([]);
});
it('enables push notifications capability with broadcast option when the capability is disabled and usesBroadcastPushNotifications is true', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => []),
updateBundleIdCapabilityAsync: jest.fn(),
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'aps-environment': 'production',
},
{
usesBroadcastPushNotifications: true,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'PUSH_NOTIFICATIONS', option: 'PUSH_NOTIFICATION_FEATURE_BROADCAST' },
]);
expect(results.enabled).toStrictEqual(['Push Notifications']);
expect(results.disabled).toStrictEqual([]);
});
it('updates push notifications capability with broadcast option when the capability is enabled without settings and usesBroadcastPushNotifications is true', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
new BundleIdCapability({}, 'XXX_PUSH_NOTIFICATIONS', { settings: null }),
]),
updateBundleIdCapabilityAsync: jest.fn(),
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'aps-environment': 'production',
},
{
usesBroadcastPushNotifications: true,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'PUSH_NOTIFICATIONS', option: 'PUSH_NOTIFICATION_FEATURE_BROADCAST' },
]);
expect(results.enabled).toStrictEqual(['Push Notifications']);
expect(results.disabled).toStrictEqual([]);
});
it('disables push notifications capability', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
new BundleIdCapability({}, 'XXX_PUSH_NOTIFICATIONS', { settings: null }),
]),
updateBundleIdCapabilityAsync: jest.fn(),
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{},
{
usesBroadcastPushNotifications: true,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'PUSH_NOTIFICATIONS', option: 'OFF' },
]);
expect(results.disabled).toStrictEqual(['Push Notifications']);
expect(results.enabled).toStrictEqual([]);
});
it('does nothing when push notifications capability is enabled with broadcast options and usesBroadcastPushNotifications is true', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
new BundleIdCapability({}, 'XXX_PUSH_NOTIFICATIONS', {
settings: [],
}),
]),
updateBundleIdCapabilityAsync: jest.fn(),
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'aps-environment': 'production',
},
{
usesBroadcastPushNotifications: true,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
expect(results.disabled).toStrictEqual([]);
expect(results.enabled).toStrictEqual([]);
});
it('does nothing when push notifications capability is enabled without broadcast options and usesBroadcastPushNotifications is false', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
new BundleIdCapability({}, 'XXX_PUSH_NOTIFICATIONS', {
settings: null,
}),
]),
updateBundleIdCapabilityAsync: jest.fn(),
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'aps-environment': 'production',
},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
expect(results.disabled).toStrictEqual([]);
expect(results.enabled).toStrictEqual([]);
});
it('updates push notifications capability without broadcast options when it is enabled and usesBroadcastPushNotifications is false', async () => {
const bundleId = {
getBundleIdCapabilitiesAsync: jest.fn(() => [
new BundleIdCapability({}, 'XXX_PUSH_NOTIFICATIONS', {
settings: [],
}),
]),
updateBundleIdCapabilityAsync: jest.fn(),
id: 'XXX',
} as any;
const results = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'aps-environment': 'production',
},
{
usesBroadcastPushNotifications: false,
}
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
{ capabilityType: 'PUSH_NOTIFICATIONS', option: 'ON' },
]);
expect(results.disabled).toStrictEqual([]);
expect(results.enabled).toStrictEqual(['Push Notifications']);
});
});
@@ -6,6 +6,7 @@ import {
CapabilityType,
CapabilityTypeDataProtectionOption,
CapabilityTypeOption,
CapabilityTypePushNotificationsOption,
CloudContainer,
MerchantId,
} from '@expo/apple-utils';
@@ -19,7 +20,10 @@ export const EXPO_NO_CAPABILITY_SYNC = getenv.boolish('EXPO_NO_CAPABILITY_SYNC',
type GetOptionsMethod<T extends CapabilityType = any> = (
entitlement: JSONValue,
entitlementsJson: JSONObject
entitlementsJson: JSONObject,
additionalOptions: {
usesBroadcastPushNotifications?: boolean;
}
) => CapabilityOptionMap[T];
const validateBooleanOptions = (options: any): boolean => {
@@ -78,7 +82,10 @@ const getDefinedOptions: GetOptionsMethod = entitlement => {
*/
export async function syncCapabilitiesForEntitlementsAsync(
bundleId: BundleId,
entitlements: JSONObject = {}
entitlements: JSONObject = {},
additionalOptions: {
usesBroadcastPushNotifications: boolean;
}
): Promise<{ enabled: string[]; disabled: string[] }> {
if (EXPO_NO_CAPABILITY_SYNC) {
return { enabled: [], disabled: [] };
@@ -91,7 +98,8 @@ export async function syncCapabilitiesForEntitlementsAsync(
const { enabledCapabilityNames, request, remainingCapabilities } = getCapabilitiesToEnable(
currentCapabilities,
entitlements
entitlements,
additionalOptions
);
const { disabledCapabilityNames, request: modifiedRequest } = getCapabilitiesToDisable(
@@ -125,9 +133,25 @@ interface CapabilitiesRequest {
option: any;
}
function shouldSkipPushNotificationsCapabilityUpdate(
existing: BundleIdCapability,
additionalOptions: {
usesBroadcastPushNotifications: boolean;
}
): boolean {
// For push notifications, we should always update the capabaility if
// - settings are not defined in the existing capability, but usesBroadcastPushNotifications is enabled (we want to add settings for this capability)
// - settings are defined in the existing capability, but usesBroadcastPushNotifications is disabled (we want to remove settings for this capability)
const noSettingsAttributes = existing.attributes.settings == null;
return !noSettingsAttributes === additionalOptions.usesBroadcastPushNotifications;
}
function getCapabilitiesToEnable(
currentCapabilities: BundleIdCapability[],
entitlements: JSONObject
entitlements: JSONObject,
additionalOptions: {
usesBroadcastPushNotifications: boolean;
}
): {
enabledCapabilityNames: string[];
request: CapabilitiesRequest[];
@@ -156,7 +180,17 @@ function getCapabilitiesToEnable(
// Only skip if the existing capability is a simple boolean value,
// if it has more complex settings then we should always update it.
// If the `existing.attributes.settings` object is defined, then we can determine that it has extra configuration.
if (existing && existing?.attributes.settings == null) {
// For push notifications, we should always update the capabaility if
// - settings are not defined in the existing capability, but usesBroadcastPushNotifications is enabled (we want to add settings for this capability)
// - settings are defined in the existing capability, but usesBroadcastPushNotifications is disabled (we want to remove settings for this capability)
const isPushNotificationsCapability =
staticCapabilityInfo.capability === CapabilityType.PUSH_NOTIFICATIONS;
if (
existing &&
((!isPushNotificationsCapability && existing.attributes.settings == null) ||
(isPushNotificationsCapability &&
shouldSkipPushNotificationsCapabilityUpdate(existing, additionalOptions)))
) {
// Remove the item from the list of capabilities so we don't disable it.
remainingCapabilities.splice(existingIndex, 1);
if (Log.isDebug) {
@@ -173,7 +207,7 @@ function getCapabilitiesToEnable(
enabledCapabilityNames.push(staticCapabilityInfo.name);
const option = staticCapabilityInfo.getOptions(value, entitlements);
const option = staticCapabilityInfo.getOptions(value, entitlements, additionalOptions);
request.push({
capabilityType: staticCapabilityInfo.capability,
@@ -488,7 +522,13 @@ export const CapabilityMapping: CapabilityClassifier[] = [
entitlement: 'aps-environment',
capability: CapabilityType.PUSH_NOTIFICATIONS,
validateOptions: validateDevProdString,
getOptions: getDefinedOptions,
getOptions(entitlement, _entitlementsJson, { usesBroadcastPushNotifications }) {
const option = entitlement ? CapabilityTypeOption.ON : CapabilityTypeOption.OFF;
if (option === CapabilityTypeOption.ON && usesBroadcastPushNotifications) {
return CapabilityTypePushNotificationsOption.PUSH_NOTIFICATION_FEATURE_BROADCAST;
}
return option;
},
},
{
name: 'Wallet',
@@ -12,6 +12,7 @@ import { ora } from '../../../ora';
export interface IosCapabilitiesOptions {
entitlements: JSONObject;
usesBroadcastPushNotifications: boolean;
}
export interface AppLookupParams {
@@ -93,7 +94,7 @@ export async function ensureBundleIdExistsWithNameAsync(
export async function syncCapabilitiesAsync(
bundleId: BundleId,
{ entitlements }: IosCapabilitiesOptions
{ entitlements, ...rest }: IosCapabilitiesOptions
): Promise<void> {
const spinner = ora(`Syncing capabilities`).start();
@@ -105,7 +106,8 @@ export async function syncCapabilitiesAsync(
try {
const { enabled, disabled } = await syncCapabilitiesForEntitlementsAsync(
bundleId,
entitlements
entitlements,
rest
);
const results =
[buildMessage('Enabled', enabled), buildMessage('Disabled', disabled)]
@@ -119,7 +121,7 @@ export async function syncCapabilitiesAsync(
}
// Always run this after syncing the capabilities...
await syncCapabilityIdentifiersAsync(bundleId, { entitlements });
await syncCapabilityIdentifiersAsync(bundleId, { entitlements, ...rest });
}
const buildMessage = (title: string, items: string[]): string =>
@@ -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,
+132 -55
View File
@@ -133,6 +133,8 @@ export type Account = {
id: Scalars['ID']['output'];
isCurrent: Scalars['Boolean']['output'];
isDisabled: Scalars['Boolean']['output'];
/** Whether an Account plan falls into AppDevDomainName's free or paid tier */
isFreeAppDevDomainTier: Scalars['Boolean']['output'];
/** Whether this account has SSO enabled. Can be queried by all members. */
isSSOEnabled: Scalars['Boolean']['output'];
lastDeletionAttemptTime?: Maybe<Scalars['DateTime']['output']>;
@@ -143,6 +145,7 @@ export type Account = {
owner?: Maybe<User>;
/** Owning UserActor of this account if personal account */
ownerUserActor?: Maybe<UserActor>;
profileImageUrl: Scalars['String']['output'];
pushSecurityEnabled: Scalars['Boolean']['output'];
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean']['output'];
@@ -711,6 +714,7 @@ export type AccountSsoConfigurationPublicDataQueryPublicDataByAccountNameArgs =
};
export enum AccountUploadSessionType {
ProfileImageUpload = 'PROFILE_IMAGE_UPLOAD',
WorkflowsProjectSources = 'WORKFLOWS_PROJECT_SOURCES'
}
@@ -1310,8 +1314,9 @@ export type App = Project & {
* @deprecated Classic updates have been deprecated.
*/
playStoreUrl?: Maybe<Scalars['String']['output']>;
/** @deprecated Use 'privacySetting' instead. */
/** @deprecated No longer supported */
privacy: Scalars['String']['output'];
/** @deprecated No longer supported */
privacySetting: AppPrivacy;
/**
* Whether there have been any classic update publishes
@@ -1364,6 +1369,8 @@ export type App = Project & {
/** EAS updates owned by an app */
updates: Array<Update>;
updatesPaginated: AppUpdatesConnection;
/** Project query object for querying EAS usage metrics */
usageMetrics: AppUsageMetrics;
/** @deprecated Use ownerAccount.name instead */
username: Scalars['String']['output'];
/** @deprecated No longer supported */
@@ -1746,7 +1753,6 @@ export type AppInfoInput = {
export type AppInput = {
accountId: Scalars['ID']['input'];
appInfo?: InputMaybe<AppInfoInput>;
privacy: AppPrivacy;
projectName: Scalars['String']['input'];
};
@@ -2048,6 +2054,48 @@ export type AppUpdatesConnection = {
pageInfo: PageInfo;
};
export enum AppUploadSessionType {
ProfileImageUpload = 'PROFILE_IMAGE_UPLOAD'
}
export type AppUsageMetric = {
__typename?: 'AppUsageMetric';
id: Scalars['ID']['output'];
metricType: UsageMetricType;
serviceMetric: EasServiceMetric;
timestamp: Scalars['DateTime']['output'];
value: Scalars['Float']['output'];
};
export type AppUsageMetricTotal = {
__typename?: 'AppUsageMetricTotal';
billingPeriod: BillingPeriod;
id: Scalars['ID']['output'];
planMetrics: Array<EstimatedUsage>;
/** Total cost of overages, in cents */
totalCost: Scalars['Float']['output'];
};
export type AppUsageMetrics = {
__typename?: 'AppUsageMetrics';
byBillingPeriod: AppUsageMetricTotal;
metricsForServiceMetric: Array<AppUsageMetric>;
};
export type AppUsageMetricsByBillingPeriodArgs = {
date: Scalars['DateTime']['input'];
service?: InputMaybe<EasService>;
};
export type AppUsageMetricsMetricsForServiceMetricArgs = {
filterParams?: InputMaybe<Scalars['JSONObject']['input']>;
granularity: UsageMetricsGranularity;
serviceMetric: EasServiceMetric;
timespan: UsageMetricsTimespan;
};
/** Represents Play Store/App Store version of an application */
export type AppVersion = {
__typename?: 'AppVersion';
@@ -2098,7 +2146,6 @@ export type AppWithGithubRepositoryInput = {
accountId: Scalars['ID']['input'];
appInfo?: InputMaybe<AppInfoInput>;
installationIdentifier?: InputMaybe<Scalars['String']['input']>;
privacy: AppPrivacy;
projectName: Scalars['String']['input'];
};
@@ -2690,6 +2737,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
error?: Maybe<BuildError>;
estimatedWaitTimeLeftSeconds?: Maybe<Scalars['Int']['output']>;
expirationDate?: Maybe<Scalars['DateTime']['output']>;
fingerprint?: Maybe<Fingerprint>;
gitCommitHash?: Maybe<Scalars['String']['output']>;
gitCommitMessage?: Maybe<Scalars['String']['output']>;
gitRef?: Maybe<Scalars['String']['output']>;
@@ -2723,6 +2771,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
reactNativeVersion?: Maybe<Scalars['String']['output']>;
releaseChannel?: Maybe<Scalars['String']['output']>;
requiredPackageManager?: Maybe<Scalars['String']['output']>;
resolvedEnvironment?: Maybe<EnvironmentVariableEnvironment>;
/**
* The builder resource class requested by the developer
* @deprecated Use resourceClassDisplayName instead
@@ -2865,6 +2914,7 @@ export type BuildFilter = {
channel?: InputMaybe<Scalars['String']['input']>;
distribution?: InputMaybe<DistributionType>;
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
hasFingerprint?: InputMaybe<Scalars['Boolean']['input']>;
platform?: InputMaybe<AppPlatform>;
runtimeVersion?: InputMaybe<Scalars['String']['input']>;
sdkVersion?: InputMaybe<Scalars['String']['input']>;
@@ -2876,6 +2926,7 @@ export type BuildFilterInput = {
channel?: InputMaybe<Scalars['String']['input']>;
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
distributions?: InputMaybe<Array<DistributionType>>;
hasFingerprint?: InputMaybe<Scalars['Boolean']['input']>;
platforms?: InputMaybe<Array<AppPlatform>>;
releaseChannel?: InputMaybe<Scalars['String']['input']>;
runtimeVersion?: InputMaybe<Scalars['String']['input']>;
@@ -3286,6 +3337,7 @@ export enum CrashSampleFor {
}
export type CrashesFilters = {
crashKind?: InputMaybe<Array<WorkerDeploymentCrashKind>>;
name?: InputMaybe<Array<Scalars['String']['input']>>;
};
@@ -3389,12 +3441,6 @@ export type CreateIosSubmissionInput = {
submittedBuildId?: InputMaybe<Scalars['ID']['input']>;
};
export type CreateServerlessFunctionUploadUrlResult = {
__typename?: 'CreateServerlessFunctionUploadUrlResult';
formDataFields: Scalars['JSONObject']['output'];
url: Scalars['String']['output'];
};
export type CreateSharedEnvironmentVariableInput = {
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
fileName?: InputMaybe<Scalars['String']['input']>;
@@ -3630,11 +3676,6 @@ export type DeleteWorkerDeploymentResult = {
id: Scalars['ID']['output'];
};
export type DeployServerlessFunctionResult = {
__typename?: 'DeployServerlessFunctionResult';
url: Scalars['String']['output'];
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type Deployment = {
__typename?: 'Deployment';
@@ -4187,6 +4228,7 @@ export type Fingerprint = {
debugInfoUrl?: Maybe<Scalars['String']['output']>;
hash: Scalars['String']['output'];
id: Scalars['ID']['output'];
source?: Maybe<FingerprintSource>;
updatedAt: Scalars['DateTime']['output'];
};
@@ -4201,6 +4243,13 @@ export type FingerprintInfoGroup = {
web?: InputMaybe<FingerprintInfo>;
};
export type FingerprintSource = {
__typename?: 'FingerprintSource';
bucketKey: Scalars['String']['output'];
isDebugFingerprint?: Maybe<Scalars['Boolean']['output']>;
type: FingerprintSourceType;
};
export type FingerprintSourceInput = {
bucketKey?: InputMaybe<Scalars['String']['input']>;
isDebugFingerprint?: InputMaybe<Scalars['Boolean']['input']>;
@@ -5507,6 +5556,7 @@ export type PublicArtifacts = {
export type PublishUpdateGroupInput = {
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']['input']>;
branchId: Scalars['String']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
excludedAssets?: InputMaybe<Array<PartialManifestAsset>>;
fingerprintInfoGroup?: InputMaybe<FingerprintInfoGroup>;
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
@@ -5536,6 +5586,7 @@ export type RequestsFilters = {
hasCustomDomainOrigin?: InputMaybe<Scalars['Boolean']['input']>;
isAsset?: InputMaybe<Scalars['Boolean']['input']>;
isCrash?: InputMaybe<Scalars['Boolean']['input']>;
isLimitExceeded?: InputMaybe<Scalars['Boolean']['input']>;
isVerifiedBot?: InputMaybe<Scalars['Boolean']['input']>;
method?: InputMaybe<Array<RequestMethod>>;
os?: InputMaybe<Array<UserAgentOs>>;
@@ -5612,8 +5663,11 @@ export type Robot = Actor & {
*/
featureGates: Scalars['JSONObject']['output'];
firstName?: Maybe<Scalars['String']['output']>;
/** GitHub App Installations that manage this actor */
githubAppInstallations: Array<GitHubAppInstallation>;
id: Scalars['ID']['output'];
isExpoAdmin: Scalars['Boolean']['output'];
isManagedByGitHubApp: Scalars['Boolean']['output'];
lastDeletionAttemptTime?: Maybe<Scalars['DateTime']['output']>;
};
@@ -5754,7 +5808,6 @@ export type RootMutation = {
notificationSubscription: NotificationSubscriptionMutation;
/** Mutations that create, update, and delete Robots */
robot: RobotMutation;
serverlessFunction: ServerlessFunctionMutation;
/** Mutations that modify an EAS Submit submission */
submission: SubmissionMutation;
update: UpdateMutation;
@@ -6157,28 +6210,6 @@ export type SecondFactorRegenerateBackupCodesResult = {
plaintextBackupCodes: Array<Scalars['String']['output']>;
};
export type ServerlessFunctionIdentifierInput = {
gitCommitSHA1: Scalars['String']['input'];
};
export type ServerlessFunctionMutation = {
__typename?: 'ServerlessFunctionMutation';
createDeployment: DeployServerlessFunctionResult;
createUploadPresignedUrl: CreateServerlessFunctionUploadUrlResult;
};
export type ServerlessFunctionMutationCreateDeploymentArgs = {
appId: Scalars['ID']['input'];
serverlessFunctionIdentifierInput: ServerlessFunctionIdentifierInput;
};
export type ServerlessFunctionMutationCreateUploadPresignedUrlArgs = {
appId: Scalars['ID']['input'];
serverlessFunctionIdentifierInput: ServerlessFunctionIdentifierInput;
};
export type Snack = Project & {
__typename?: 'Snack';
/** Description of the Snack */
@@ -6401,7 +6432,7 @@ export enum SubmissionAndroidTrack {
}
export type SubmissionArchiveSourceInput = {
/** Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE or GCS_SUBMIT_ARCHIVE */
/** Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE, GCS_BUILD_APPLICATION_ARCHIVE_ORCHESTRATOR or GCS_SUBMIT_ARCHIVE */
bucketKey?: InputMaybe<Scalars['String']['input']>;
type: SubmissionArchiveSourceType;
/** Required if the archive source type is URL */
@@ -6410,6 +6441,7 @@ export type SubmissionArchiveSourceInput = {
export enum SubmissionArchiveSourceType {
GcsBuildApplicationArchive = 'GCS_BUILD_APPLICATION_ARCHIVE',
GcsBuildApplicationArchiveOrchestrator = 'GCS_BUILD_APPLICATION_ARCHIVE_ORCHESTRATOR',
GcsSubmitArchive = 'GCS_SUBMIT_ARCHIVE',
Url = 'URL'
}
@@ -6567,6 +6599,7 @@ export type Update = ActivityTimelineProjectActivity & {
codeSigningInfo?: Maybe<CodeSigningInfo>;
createdAt: Scalars['DateTime']['output'];
deployments: DeploymentResult;
environment?: Maybe<EnvironmentVariableEnvironment>;
expoGoSDKVersion?: Maybe<Scalars['String']['output']>;
fingerprint?: Maybe<Fingerprint>;
gitCommitHash?: Maybe<Scalars['String']['output']>;
@@ -6882,6 +6915,8 @@ export type UploadSession = {
__typename?: 'UploadSession';
/** Create an Upload Session for a specific account */
createAccountScopedUploadSession: Scalars['JSONObject']['output'];
/** Create an Upload Session for a specific app */
createAppScopedUploadSession: Scalars['JSONObject']['output'];
/** Create an Upload Session */
createUploadSession: Scalars['JSONObject']['output'];
};
@@ -6893,6 +6928,12 @@ export type UploadSessionCreateAccountScopedUploadSessionArgs = {
};
export type UploadSessionCreateAppScopedUploadSessionArgs = {
appID: Scalars['ID']['input'];
type: AppUploadSessionType;
};
export type UploadSessionCreateUploadSessionArgs = {
type: UploadSessionType;
};
@@ -7651,6 +7692,7 @@ export type WorkerDeployment = ActivityTimelineProjectActivity & {
activityTimestamp: Scalars['DateTime']['output'];
actor?: Maybe<Actor>;
aliases?: Maybe<Array<WorkerDeploymentAlias>>;
app: App;
crashes?: Maybe<WorkerDeploymentCrashes>;
createdAt: Scalars['DateTime']['output'];
deploymentDomain: Scalars['String']['output'];
@@ -7661,7 +7703,6 @@ export type WorkerDeployment = ActivityTimelineProjectActivity & {
logs?: Maybe<WorkerDeploymentLogs>;
requests?: Maybe<WorkerDeploymentRequests>;
subdomain: Scalars['String']['output'];
tier?: Maybe<WorkerDeploymentTier>;
url: Scalars['String']['output'];
};
@@ -7715,9 +7756,19 @@ export type WorkerDeploymentCrashEdge = {
request?: Maybe<WorkerDeploymentRequestNode>;
};
export enum WorkerDeploymentCrashKind {
ExceededCpu = 'EXCEEDED_CPU',
ExceededMemory = 'EXCEEDED_MEMORY',
ExceededSubrequests = 'EXCEEDED_SUBREQUESTS',
Generic = 'GENERIC',
Internal = 'INTERNAL',
ResponseStreamDisconnected = 'RESPONSE_STREAM_DISCONNECTED'
}
export type WorkerDeploymentCrashNode = {
__typename?: 'WorkerDeploymentCrashNode';
crashHash: Scalars['ID']['output'];
crashKind: WorkerDeploymentCrashKind;
crashTimestamp: Scalars['DateTime']['output'];
deploymentIdentifier: Scalars['String']['output'];
firstStackLine?: Maybe<Scalars['String']['output']>;
@@ -7829,6 +7880,7 @@ export type WorkerDeploymentRequestNode = {
hasCustomDomainOrigin: Scalars['Boolean']['output'];
isAsset: Scalars['Boolean']['output'];
isCrash: Scalars['Boolean']['output'];
isLimitExceeded: Scalars['Boolean']['output'];
isRejected: Scalars['Boolean']['output'];
isStaleIfError: Scalars['Boolean']['output'];
isStaleWhileRevalidate: Scalars['Boolean']['output'];
@@ -7939,6 +7991,8 @@ export type WorkerDeploymentRequestsAggregationNode = {
durationP50: Scalars['Float']['output'];
durationP90: Scalars['Float']['output'];
durationP99: Scalars['Float']['output'];
limitExceededPerMs?: Maybe<Scalars['Float']['output']>;
limitExceededSum: Scalars['Int']['output'];
requestsPerMs?: Maybe<Scalars['Float']['output']>;
requestsSum: Scalars['Int']['output'];
sampleRate?: Maybe<Scalars['Float']['output']>;
@@ -8014,11 +8068,6 @@ export type WorkerDeploymentRequestsTimeseriesEdge = {
timestamp: Scalars['DateTime']['output'];
};
export enum WorkerDeploymentTier {
Free = 'FREE',
Paid = 'PAID'
}
export type WorkerDeploymentsConnection = {
__typename?: 'WorkerDeploymentsConnection';
edges: Array<WorkerDeploymentEdge>;
@@ -8113,6 +8162,7 @@ export enum WorkflowJobStatus {
Failure = 'FAILURE',
InProgress = 'IN_PROGRESS',
New = 'NEW',
PendingCancel = 'PENDING_CANCEL',
Skipped = 'SKIPPED',
Success = 'SUCCESS'
}
@@ -8121,6 +8171,7 @@ export enum WorkflowJobType {
AppleDeviceRegistrationRequest = 'APPLE_DEVICE_REGISTRATION_REQUEST',
Build = 'BUILD',
Custom = 'CUSTOM',
Deploy = 'DEPLOY',
GetBuild = 'GET_BUILD',
MaestroTest = 'MAESTRO_TEST',
RequireApproval = 'REQUIRE_APPROVAL',
@@ -8130,6 +8181,7 @@ export enum WorkflowJobType {
export type WorkflowProjectSourceInput = {
easJsonBucketKey: Scalars['String']['input'];
packageJsonBucketKey?: InputMaybe<Scalars['String']['input']>;
projectArchiveBucketKey: Scalars['String']['input'];
type: WorkflowProjectSourceType;
};
@@ -8203,6 +8255,7 @@ export type WorkflowRun = ActivityTimelineProjectActivity & {
activityTimestamp: Scalars['DateTime']['output'];
actor?: Maybe<Actor>;
createdAt: Scalars['DateTime']['output'];
errors: Array<WorkflowRunError>;
gitCommitHash?: Maybe<Scalars['String']['output']>;
gitCommitMessage?: Maybe<Scalars['String']['output']>;
githubRepository?: Maybe<GitHubRepository>;
@@ -8224,16 +8277,28 @@ export type WorkflowRunEdge = {
node: WorkflowRun;
};
export type WorkflowRunError = {
__typename?: 'WorkflowRunError';
message: Scalars['String']['output'];
title?: Maybe<Scalars['String']['output']>;
};
export type WorkflowRunInput = {
projectSource: WorkflowProjectSourceInput;
};
export type WorkflowRunMutation = {
__typename?: 'WorkflowRunMutation';
cancelWorkflowRun: WorkflowRun;
createWorkflowRun: WorkflowRun;
};
export type WorkflowRunMutationCancelWorkflowRunArgs = {
workflowRunId: Scalars['ID']['input'];
};
export type WorkflowRunMutationCreateWorkflowRunArgs = {
appId: Scalars['ID']['input'];
workflowRevisionInput: WorkflowRevisionInput;
@@ -8256,6 +8321,7 @@ export enum WorkflowRunStatus {
Failure = 'FAILURE',
InProgress = 'IN_PROGRESS',
New = 'NEW',
PendingCancel = 'PENDING_CANCEL',
Success = 'SUCCESS'
}
@@ -8938,7 +9004,7 @@ export type UpdatePublishMutationVariables = Exact<{
}>;
export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> } };
export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> } };
export type SetCodeSigningInfoMutationVariables = Exact<{
updateId: Scalars['ID']['input'];
@@ -8954,7 +9020,7 @@ export type SetRolloutPercentageMutationVariables = Exact<{
}>;
export type SetRolloutPercentageMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', setRolloutPercentage: { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null } } };
export type SetRolloutPercentageMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', setRolloutPercentage: { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
export type CreateAndroidSubmissionMutationVariables = Exact<{
appId: Scalars['ID']['input'];
@@ -9093,7 +9159,7 @@ export type BranchesByAppQueryVariables = Exact<{
}>;
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> }> } } };
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> }> } } };
export type BranchesBasicPaginatedOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9114,7 +9180,7 @@ export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
}>;
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }>> }> } | null } } };
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
export type BuildsByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
@@ -9130,6 +9196,13 @@ export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithFingerprintByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
}>;
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
offset: Scalars['Int']['input'];
@@ -9147,7 +9220,7 @@ export type ViewUpdateChannelOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }>> }> } | null } } };
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9156,7 +9229,7 @@ export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }>> }> }> } } };
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> }> } } };
export type ViewUpdateChannelsPaginatedOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9280,7 +9353,7 @@ export type ViewUpdatesByGroupQueryVariables = Exact<{
}>;
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> };
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9291,7 +9364,7 @@ export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
}>;
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }>> } | null } } };
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } | null } } };
export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9301,7 +9374,7 @@ export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }>> } } };
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } } };
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
@@ -9331,21 +9404,25 @@ export type BuildFragment = { __typename?: 'Build', id: string, status: BuildSta
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType };
export type EnvironmentVariableWithSecretFragment = { __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType };
export type FingerprintFragment = { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null };
export type RuntimeFragment = { __typename?: 'Runtime', id: string, version: string };
export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id: string, name: StatuspageServiceName, status: StatuspageServiceStatus, incidents: Array<{ __typename?: 'StatuspageIncident', id: string, status: StatuspageIncidentStatus, name: string, impact: StatuspageIncidentImpact, shortlink: string }> };
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null };
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null };
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> };
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
export type UpdateBranchBasicInfoFragment = { __typename?: 'UpdateBranch', id: string, name: string };
@@ -3,7 +3,7 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import { AppPrivacy, CreateAppMutation, CreateAppMutationVariables } from '../generated';
import { CreateAppMutation, CreateAppMutationVariables } from '../generated';
export const AppMutation = {
async createAppAsync(
@@ -11,7 +11,6 @@ export const AppMutation = {
appInput: {
accountId: string;
projectName: string;
privacy: AppPrivacy;
}
): Promise<string> {
const data = await withErrorHandlingAsync(
@@ -24,6 +24,7 @@ type CreateVariableArgs = {
visibility: EnvironmentVariableVisibility;
environments: EnvironmentVariableEnvironment[];
type: EnvironmentSecretType;
isGlobal?: boolean;
fileName?: string;
};
@@ -5,15 +5,22 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import { withErrorHandlingAsync } from '../client';
import {
BuildFragment,
BuildWithFingerprintFragment,
BuildWithSubmissionsFragment,
BuildsByIdQuery,
BuildsByIdQueryVariables,
BuildsWithFingerprintByIdQuery,
BuildsWithFingerprintByIdQueryVariables,
BuildsWithSubmissionsByIdQuery,
BuildsWithSubmissionsByIdQueryVariables,
ViewBuildsOnAppQuery,
ViewBuildsOnAppQueryVariables,
} from '../generated';
import { BuildFragmentNode, BuildFragmentWithSubmissionsNode } from '../types/Build';
import {
BuildFragmentNode,
BuildFragmentWithFingerprintNode,
BuildFragmentWithSubmissionsNode,
} from '../types/Build';
export const BuildQuery = {
async byIdAsync(
@@ -76,6 +83,36 @@ export const BuildQuery = {
return data.builds.byId;
},
async withFingerprintByIdAsync(
graphqlClient: ExpoGraphqlClient,
buildId: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<BuildWithFingerprintFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BuildsWithFingerprintByIdQuery, BuildsWithFingerprintByIdQueryVariables>(
gql`
query BuildsWithFingerprintByIdQuery($buildId: ID!) {
builds {
byId(buildId: $buildId) {
id
...BuildWithFingerprintFragment
}
}
}
${print(BuildFragmentWithFingerprintNode)}
`,
{ buildId },
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['Build'],
}
)
.toPromise()
);
return data.builds.byId;
},
async viewBuildsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, limit, offset, filter }: ViewBuildsOnAppQueryVariables
@@ -1,6 +1,7 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { FingerprintFragmentNode } from './Fingerprint';
import { SubmissionFragmentNode } from './Submission';
export const BuildFragmentNode = gql`
@@ -72,3 +73,17 @@ export const BuildFragmentWithSubmissionsNode = gql`
}
}
`;
export const BuildFragmentWithFingerprintNode = gql`
${print(FingerprintFragmentNode)}
${print(BuildFragmentNode)}
fragment BuildWithFingerprintFragment on Build {
id
...BuildFragment
fingerprint {
id
...FingerprintFragment
}
}
`;
@@ -0,0 +1,9 @@
import gql from 'graphql-tag';
export const FingerprintFragmentNode = gql`
fragment FingerprintFragment on Fingerprint {
id
hash
debugInfoUrl
}
`;
@@ -35,5 +35,14 @@ export const UpdateFragmentNode = gql`
rolloutControlUpdate {
id
}
fingerprint {
id
hash
source {
type
bucketKey
isDebugFingerprint
}
}
}
`;
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import { getProjectDashboardUrl } from '../build/utils/url';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppPrivacy, Role } from '../graphql/generated';
import { Role } from '../graphql/generated';
import { AppMutation } from '../graphql/mutations/AppMutation';
import { AppQuery } from '../graphql/queries/AppQuery';
import { link } from '../log';
@@ -21,7 +21,6 @@ export async function fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsyn
projectInfo: {
accountName: string;
projectName: string;
privacy?: AppPrivacy;
},
options: {
nonInteractive: boolean;
@@ -85,14 +84,13 @@ export async function fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsyn
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
const projectLink = link(projectDashboardUrl, { text: projectFullName });
const spinner = ora(`Creating ${chalk.bold(projectFullName)} on Expo`).start();
const spinner = ora(`Creating ${chalk.bold(projectFullName)} on EAS`).start();
try {
const id = await AppMutation.createAppAsync(graphqlClient, {
accountId: account.id,
projectName,
privacy: projectInfo.privacy ?? AppPrivacy.Public,
});
spinner.succeed(`Created ${chalk.bold(projectLink)} on Expo`);
spinner.succeed(`Created ${chalk.bold(projectLink)} on EAS`);
return id;
} catch (err) {
spinner.fail();
+1 -11
View File
@@ -7,7 +7,7 @@ import semver from 'semver';
import { getEASUpdateURL } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AccountFragment, AppPrivacy } from '../graphql/generated';
import { AccountFragment } from '../graphql/generated';
import { AppQuery } from '../graphql/queries/AppQuery';
import Log, { learnMore } from '../log';
import { Actor } from '../user/User';
@@ -31,16 +31,6 @@ export function getUsername(exp: ExpoConfig, user: Actor): string | undefined {
}
}
export const toAppPrivacy = (privacy: ExpoConfig['privacy']): AppPrivacy => {
if (privacy === 'public') {
return AppPrivacy.Public;
} else if (privacy === 'hidden') {
return AppPrivacy.Hidden;
} else {
return AppPrivacy.Unlisted;
}
};
/**
* Return a useful name describing the project config.
* - dynamic: app.config.js
+134 -18
View File
@@ -1,6 +1,6 @@
import { ExpoConfig, Platform as ExpoConfigPlatform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { Env, FingerprintSource, Platform, Workflow } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import chalk from 'chalk';
@@ -12,6 +12,7 @@ import nullthrows from 'nullthrows';
import path from 'path';
import promiseLimit from 'promise-limit';
import { maybeUploadFingerprintAsync } from './maybeUploadFingerprintAsync';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from './projectUtils';
import { resolveRuntimeVersionUsingCLIAsync } from './resolveRuntimeVersionAsync';
import { selectBranchOnAppAsync } from '../branch/queries';
@@ -47,7 +48,9 @@ import { ExpoUpdatesCLIModuleNotFoundError } from '../utils/expoUpdatesCli';
import chunk from '../utils/expodash/chunk';
import { truthy } from '../utils/expodash/filter';
import groupBy from '../utils/expodash/groupBy';
import mapMapAsync from '../utils/expodash/mapMapAsync';
import uniqBy from '../utils/expodash/uniqBy';
import { FingerprintOptions, createFingerprintsByKeyAsync } from '../utils/fingerprintCli';
import { Client } from '../vcs/vcs';
// update publish does not currently support web
@@ -631,7 +634,13 @@ export async function getBranchNameForCommandAsync({
}
if (autoFlag) {
return await getDefaultBranchNameAsync(vcsClient);
const defaultBranchNameFromVcs = await getDefaultBranchNameAsync(vcsClient);
if (!defaultBranchNameFromVcs) {
throw new Error(
'Must supply --branch or --channel for branch name as auto-detection of branch name via --auto is not supported when no VCS is present.'
);
}
return defaultBranchNameFromVcs;
} else if (nonInteractive) {
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode.');
} else {
@@ -655,7 +664,7 @@ export async function getBranchNameForCommandAsync({
type: 'text',
name: 'name',
message: 'No branches found. Provide a branch name:',
initial: await getDefaultBranchNameAsync(vcsClient),
initial: (await getDefaultBranchNameAsync(vcsClient)) ?? undefined,
validate: value => (value ? true : 'Branch name may not be empty.'),
});
branchName = name;
@@ -728,6 +737,16 @@ export type RuntimeVersionInfo = {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
};
type FingerprintInfoGroup = {
[key in UpdatePublishPlatform]?: FingerprintInfo;
};
type FingerprintInfo = {
fingerprintHash: string;
fingerprintSource: FingerprintSource;
};
export async function getRuntimeVersionInfoObjectsAsync({
@@ -782,6 +801,7 @@ async function getRuntimeVersionInfoForPlatformAsync({
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
}> {
if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) {
try {
@@ -833,6 +853,7 @@ async function getRuntimeVersionInfoForPlatformAsync({
return {
runtimeVersion: resolvedRuntimeVersion,
fingerprint: null,
fingerprintHash: null,
};
}
@@ -858,11 +879,121 @@ export function getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersion
runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.fingerprint
)[0] ?? null,
fingerprintHash:
runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.fingerprintHash
)[0] ?? null,
};
}
);
}
export async function maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync({
projectDir,
graphqlClient,
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping,
workflowsByPlatform,
env,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
fingerprintSource: FingerprintSource | null;
})[];
workflowsByPlatform: Record<Platform, Workflow>;
env: Env | undefined;
}): Promise<
(RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
fingerprintSource: FingerprintSource | null;
fingerprintInfoGroup: FingerprintInfoGroup;
})[]
> {
const runtimesToComputeFingerprintsFor =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.filter(
infoGroup => !infoGroup.fingerprintHash
);
const fingerprintOptionsByRuntimeAndPlatform = new Map<string, FingerprintOptions>();
for (const infoGroup of runtimesToComputeFingerprintsFor) {
for (const platform of infoGroup.platforms) {
const runtimeAndPlatform = `${infoGroup.runtimeVersion}-${platform}`;
const options = {
platforms: [platform],
workflow: workflowsByPlatform[platform],
projectDir,
env,
};
fingerprintOptionsByRuntimeAndPlatform.set(runtimeAndPlatform, options);
}
}
const fingerprintsByRuntimeAndPlatform = await createFingerprintsByKeyAsync(
projectDir,
fingerprintOptionsByRuntimeAndPlatform
);
const uploadedFingerprintsByRuntimeAndPlatform = await mapMapAsync(
fingerprintsByRuntimeAndPlatform,
async fingerprint => {
return {
...fingerprint,
uploadedSource: (
await maybeUploadFingerprintAsync({
hash: fingerprint.hash,
fingerprint: {
fingerprintSources: fingerprint.sources,
isDebugFingerprintSource: fingerprint.isDebugSource,
},
graphqlClient,
})
).fingerprintSource,
};
}
);
const runtimesWithComputedFingerprint = runtimesToComputeFingerprintsFor.map(runtimeInfo => {
const fingerprintInfoGroup: FingerprintInfoGroup = {};
for (const platform of runtimeInfo.platforms) {
const runtimeAndPlatform = `${runtimeInfo.runtimeVersion}-${platform}`;
const fingerprint = uploadedFingerprintsByRuntimeAndPlatform.get(runtimeAndPlatform);
if (fingerprint && fingerprint.uploadedSource) {
fingerprintInfoGroup[platform] = {
fingerprintHash: fingerprint.hash,
fingerprintSource: fingerprint.uploadedSource,
};
}
}
return {
...runtimeInfo,
fingerprintInfoGroup,
};
});
// These are runtimes whose fingerprint has already been computed and uploaded with EAS Update fingerprint runtime policy
const runtimesWithPreviouslyComputedFingerprints =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping
.filter(
(
infoGroup
): infoGroup is RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
fingerprintSource: FingerprintSource;
fingerprintHash: string;
} => !!infoGroup.fingerprintHash && !!infoGroup.fingerprintSource
)
.map(infoGroup => {
const platform = infoGroup.platforms[0];
return {
...infoGroup,
fingerprintInfoGroup: {
[platform]: {
fingerprintHash: infoGroup.fingerprintHash,
fingerprintSource: infoGroup.fingerprintSource,
},
},
};
});
return [...runtimesWithComputedFingerprint, ...runtimesWithPreviouslyComputedFingerprints];
}
export const platformDisplayNames: Record<UpdatePublishPlatform, string> = {
android: 'Android',
ios: 'iOS',
@@ -873,21 +1004,6 @@ export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, A
ios: AppPlatform.Ios,
};
const mapMapAsync = async function <K, V, M>(
map: ReadonlyMap<K, V>,
mapper: (value: V, key: K) => Promise<M>
): Promise<Map<K, M>> {
const resultingMap: Map<K, M> = new Map();
await Promise.all(
Array.from(map.keys()).map(async k => {
const initialValue = map.get(k) as V;
const result = await mapper(initialValue, k);
resultingMap.set(k, result);
})
);
return resultingMap;
};
export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -27,6 +27,7 @@ export async function resolveRuntimeVersionUsingCLIAsync({
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
}> {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
@@ -52,6 +53,9 @@ export async function resolveRuntimeVersionUsingCLIAsync({
isDebugFingerprintSource: useDebugFingerprintSource,
}
: null,
fingerprintHash: runtimeVersionResult.fingerprintSources
? runtimeVersionResult.runtimeVersion
: null,
};
}
@@ -75,6 +79,7 @@ export async function resolveRuntimeVersionAsync({
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
} | null> {
if (!(await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir))) {
// fall back to the previous behavior (using the @expo/config-plugins eas-cli dependency rather
@@ -82,6 +87,7 @@ export async function resolveRuntimeVersionAsync({
return {
runtimeVersion: await Updates.getRuntimeVersionNullableAsync(projectDir, exp, platform),
fingerprint: null,
fingerprintHash: null,
};
}
@@ -1,47 +0,0 @@
import chalk from 'chalk';
import fs from 'node:fs';
import path from 'node:path';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AccountUploadSessionType } from '../graphql/generated';
import { uploadAccountScopedFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
/**
* Uploads the `eas.json` file to GCS as account-scoped object.
* Used in workflows. Takes care of logging progress.
*/
export async function uploadAccountScopedEasJsonAsync({
graphqlClient,
accountId,
projectDir,
}: {
graphqlClient: ExpoGraphqlClient;
accountId: string;
projectDir: string;
}): Promise<{ easJsonBucketKey: string }> {
const easJsonFilePath = path.join(projectDir, 'eas.json');
const easJsonFileStat = await fs.promises.stat(easJsonFilePath);
if (easJsonFileStat.size > 1024 * 1024) {
throw new Error('eas.json is too big. Maximum allowed size is 1MB.');
}
const easJsonBucketKey = await uploadAccountScopedFileAtPathToGCSAsync(graphqlClient, {
accountId,
type: AccountUploadSessionType.WorkflowsProjectSources,
path: easJsonFilePath,
handleProgressEvent: createProgressTracker({
total: easJsonFileStat.size,
message: ratio =>
`Uploading eas.json to EAS (${formatBytes(easJsonFileStat.size * ratio)} / ${formatBytes(
easJsonFileStat.size
)})`,
completedMessage: (duration: string) => `Uploaded eas.json to EAS ${chalk.dim(duration)}`,
}),
});
return { easJsonBucketKey };
}
@@ -0,0 +1,49 @@
import chalk from 'chalk';
import fs from 'node:fs';
import path from 'node:path';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AccountUploadSessionType } from '../graphql/generated';
import { uploadAccountScopedFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { createProgressTracker } from '../utils/progress';
/**
* Uploads a file to GCS as account-scoped object.
* Used in workflows. Takes care of logging progress.
* (Uses file name when mentioning file in logs.)
*/
export async function uploadAccountScopedFileAsync({
graphqlClient,
accountId,
filePath,
maxSizeBytes,
}: {
graphqlClient: ExpoGraphqlClient;
accountId: string;
filePath: string;
maxSizeBytes: number;
}): Promise<{ fileBucketKey: string }> {
const fileName = path.basename(filePath);
const fileStat = await fs.promises.stat(filePath);
if (fileStat.size > maxSizeBytes) {
throw new Error(`File is too big. Maximum allowed size is ${formatBytes(maxSizeBytes)}.`);
}
const fileBucketKey = await uploadAccountScopedFileAtPathToGCSAsync(graphqlClient, {
accountId,
type: AccountUploadSessionType.WorkflowsProjectSources,
path: filePath,
handleProgressEvent: createProgressTracker({
total: fileStat.size,
message: ratio =>
`Uploading ${fileName} to EAS (${formatBytes(fileStat.size * ratio)} / ${formatBytes(
fileStat.size
)})`,
completedMessage: (duration: string) => `Uploaded ${fileName} to EAS ${chalk.dim(duration)}`,
}),
});
return { fileBucketKey };
}
@@ -149,7 +149,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
return newChannelInfo;
}
async confirmCreationAsync(ctx: EASUpdateContext): Promise<boolean> {
private async confirmCreationAsync(ctx: EASUpdateContext): Promise<boolean> {
const { nonInteractive } = ctx;
if (nonInteractive) {
return true;
@@ -159,7 +159,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
});
}
async getChannelObjectAsync(
private async getChannelObjectAsync(
ctx: EASUpdateContext,
runtimeVersion: string
): Promise<UpdateChannelObject> {
@@ -172,7 +172,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
});
}
async getLatestUpdateGroupOnBranchAsync(
private async getLatestUpdateGroupOnBranchAsync(
ctx: EASUpdateContext,
branchInfo: UpdateBranchBasicInfoFragment,
runtimeVersion: string
@@ -195,7 +195,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
return updateGroups[0] ?? null;
}
async selectRuntimeVersionAsync(
private async selectRuntimeVersionAsync(
ctx: EASUpdateContext,
branchToRollout: UpdateBranchBasicInfoFragment,
defaultBranchId: string
@@ -223,7 +223,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
);
}
async selectRuntimeVersionFromAlternativeSourceAsync(
private async selectRuntimeVersionFromAlternativeSourceAsync(
ctx: EASUpdateContext,
branchToRollout: UpdateBranchBasicInfoFragment,
defaultBranch: UpdateBranchBasicInfoFragment
@@ -271,7 +271,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
);
}
async selectRuntimeVersionFromProjectConfigAsync(ctx: EASUpdateContext): Promise<string> {
private async selectRuntimeVersionFromProjectConfigAsync(ctx: EASUpdateContext): Promise<string> {
const platforms: ('ios' | 'android')[] = ['ios', 'android'];
const workflows = await resolveWorkflowPerPlatformAsync(ctx.app.projectDir, ctx.vcsClient);
const runtimes = (
@@ -313,7 +313,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
return selectedRuntime;
}
async selectBranchAsync(
private async selectBranchAsync(
ctx: EASUpdateContext,
defaultBranchId: string
): Promise<UpdateBranchBasicInfoFragment> {
@@ -333,7 +333,7 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
return branchInfo;
}
async resolveBranchNameAsync(
private async resolveBranchNameAsync(
ctx: EASUpdateContext,
branchName: string
): Promise<UpdateBranchBasicInfoFragment> {
@@ -86,7 +86,7 @@ export class EditRollout implements EASUpdateAction<UpdateChannelBasicInfoFragme
return newChannelInfo;
}
async confirmEditAsync(ctx: EASUpdateContext): Promise<boolean> {
private async confirmEditAsync(ctx: EASUpdateContext): Promise<boolean> {
const { nonInteractive } = ctx;
if (nonInteractive) {
return true;
@@ -96,7 +96,7 @@ export class EditRollout implements EASUpdateAction<UpdateChannelBasicInfoFragme
});
}
async getChannelObjectAsync(ctx: EASUpdateContext): Promise<UpdateChannelObject> {
private async getChannelObjectAsync(ctx: EASUpdateContext): Promise<UpdateChannelObject> {
const { graphqlClient, app } = ctx;
const { projectId } = app;
if (!isRollout(this.channelInfo)) {
@@ -87,7 +87,7 @@ export class EndRollout implements EASUpdateAction<UpdateChannelBasicInfoFragmen
return await this.performOutcomeAsync(ctx, rollout, outcome);
}
async getChannelObjectAsync(ctx: EASUpdateContext): Promise<UpdateChannelObject> {
private async getChannelObjectAsync(ctx: EASUpdateContext): Promise<UpdateChannelObject> {
const { graphqlClient, app } = ctx;
const { projectId } = app;
if (!isRollout(this.channelInfo)) {
@@ -107,7 +107,7 @@ export class EndRollout implements EASUpdateAction<UpdateChannelBasicInfoFragmen
});
}
async selectOutcomeAsync(rollout: Rollout<UpdateBranchObject>): Promise<EndOutcome> {
private async selectOutcomeAsync(rollout: Rollout<UpdateBranchObject>): Promise<EndOutcome> {
const { rolledOutBranch, percentRolledOut, defaultBranch } = rollout;
const rolledOutUpdateGroup = rolledOutBranch.updateGroups[0];
const defaultUpdateGroup = defaultBranch.updateGroups[0];
@@ -144,7 +144,7 @@ export class EndRollout implements EASUpdateAction<UpdateChannelBasicInfoFragmen
return selectedOutcome;
}
async performOutcomeAsync(
private async performOutcomeAsync(
ctx: EASUpdateContext,
rollout: Rollout<UpdateBranchObject>,
outcome: EndOutcome
@@ -185,7 +185,7 @@ export class EndRollout implements EASUpdateAction<UpdateChannelBasicInfoFragmen
return newChannelInfo;
}
async confirmOutcomeAsync(
private async confirmOutcomeAsync(
ctx: EASUpdateContext,
selectedOutcome: EndOutcome,
rollout: Rollout<UpdateBranchObject>
@@ -61,7 +61,7 @@ export class ManageRollout implements EASUpdateAction<EASUpdateAction> {
}
}
async selectActionAsync(): Promise<ManageRolloutActions> {
private async selectActionAsync(): Promise<ManageRolloutActions> {
const manageOptions = [ManageRolloutActions.EDIT, ManageRolloutActions.END];
if (this.options.callingAction) {
manageOptions.push(ManageRolloutActions.GO_BACK);
@@ -78,7 +78,7 @@ export class ManageRollout implements EASUpdateAction<EASUpdateAction> {
return selectedManageOption;
}
async getChannelObjectAsync(ctx: EASUpdateContext): Promise<UpdateChannelObject> {
private async getChannelObjectAsync(ctx: EASUpdateContext): Promise<UpdateChannelObject> {
const { graphqlClient, app } = ctx;
const { projectId } = app;
if (!isRollout(this.channelInfo)) {
@@ -91,7 +91,7 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
}
}
async runActionAsync(
private async runActionAsync(
ctx: EASUpdateContext,
action: RolloutActions,
channelObject: UpdateChannelObject
@@ -108,7 +108,7 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
}
}
viewRollout(channelObject: UpdateChannelObject): UpdateChannelObject {
private viewRollout(channelObject: UpdateChannelObject): UpdateChannelObject {
if (!this.options.json) {
printRollout(channelObject);
Log.warn('For formatted output, add the --json flag to your command.');
@@ -116,7 +116,7 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
return channelObject;
}
async getJsonAsync({
private async getJsonAsync({
originalChannelObject,
updatedChannelObject,
}: {
@@ -134,7 +134,9 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
};
}
async getRolloutJsonAsync(channelObject: UpdateChannelObject): Promise<JSONRolloutOutput> {
private async getRolloutJsonAsync(
channelObject: UpdateChannelObject
): Promise<JSONRolloutOutput> {
const rollout = getRollout(channelObject);
return {
defaultBranch: rollout.defaultBranch,
@@ -145,7 +147,7 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
};
}
getRuntimeVersion(channelInfo: UpdateChannelBasicInfoFragment): string | undefined {
private getRuntimeVersion(channelInfo: UpdateChannelBasicInfoFragment): string | undefined {
if (isRollout(channelInfo)) {
const updatedRolloutInfo = getRolloutInfo(channelInfo);
if (isConstrainedRolloutInfo(updatedRolloutInfo)) {
@@ -155,7 +157,7 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
return undefined;
}
async getChannelObjectAsync(
private async getChannelObjectAsync(
ctx: EASUpdateContext,
{ channelName, runtimeVersion }: { channelName: string; runtimeVersion?: string }
): Promise<UpdateChannelObject> {
@@ -54,7 +54,7 @@ export class RolloutMainMenu implements EASUpdateAction<void> {
await this.runActionAsync(ctx, menuOption);
}
async runActionAsync(ctx: EASUpdateContext, menuAction: MainMenuActions): Promise<null> {
private async runActionAsync(ctx: EASUpdateContext, menuAction: MainMenuActions): Promise<null> {
const { channelName } = this.options;
switch (menuAction) {
case MainMenuActions.CREATE_NEW: {
@@ -87,13 +87,15 @@ export class RolloutMainMenu implements EASUpdateAction<void> {
}
}
async selectRolloutAsync(ctx: EASUpdateContext): Promise<UpdateChannelBasicInfoFragment | null> {
private async selectRolloutAsync(
ctx: EASUpdateContext
): Promise<UpdateChannelBasicInfoFragment | null> {
const selectRollout = new SelectRollout();
const channelInfo = await selectRollout.runAsync(ctx);
return channelInfo;
}
async selectChannelToRolloutAsync(
private async selectChannelToRolloutAsync(
ctx: EASUpdateContext
): Promise<UpdateChannelBasicInfoFragment> {
let hasSomeChannel = false;
@@ -113,7 +115,7 @@ export class RolloutMainMenu implements EASUpdateAction<void> {
return channelInfo;
}
async resolveChannelNameAsync(
private async resolveChannelNameAsync(
ctx: EASUpdateContext,
channelName: string
): Promise<UpdateChannelBasicInfoFragment> {
@@ -124,7 +126,7 @@ export class RolloutMainMenu implements EASUpdateAction<void> {
});
}
toMainMenuAction(action: RolloutActions): MainMenuActions {
private toMainMenuAction(action: RolloutActions): MainMenuActions {
if (action === MainMenuActions.CREATE_NEW) {
return MainMenuActions.CREATE_NEW;
} else if (
@@ -138,7 +140,7 @@ export class RolloutMainMenu implements EASUpdateAction<void> {
}
}
async promptMenuActionAsync(): Promise<MainMenuActions> {
private async promptMenuActionAsync(): Promise<MainMenuActions> {
const menuOptions = [MainMenuActions.CREATE_NEW, MainMenuActions.MANAGE_EXISTING];
const { menuOption: selectedMenuOption } = await promptAsync({
type: 'select',
@@ -27,7 +27,7 @@ export class SelectRuntime implements EASUpdateAction<string | null> {
: `runtime`;
}
warnNoRuntime(): void {
private warnNoRuntime(): void {
if (this.options.anotherBranchToIntersectRuntimesBy) {
const intersectBranchName = this.options.anotherBranchToIntersectRuntimesBy.name;
Log.warn(
@@ -42,7 +42,7 @@ export class SelectRuntime implements EASUpdateAction<string | null> {
}
}
formatCantFindRuntime(): string {
private formatCantFindRuntime(): string {
return `🕵️ Not finding the update you were looking for? ${learnMore(
'https://expo.fyi/eas-update-rollouts'
)}`;
@@ -104,7 +104,7 @@ export class SelectRuntime implements EASUpdateAction<string | null> {
return selectedRuntime.version;
}
async getNewestRuntimeAsync(
private async getNewestRuntimeAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
@@ -126,7 +126,7 @@ export class SelectRuntime implements EASUpdateAction<string | null> {
});
}
async displayLatestUpdateGroupAsync({
private async displayLatestUpdateGroupAsync({
graphqlClient,
appId,
branchName,
@@ -153,7 +153,7 @@ export class SelectRuntime implements EASUpdateAction<string | null> {
return formatRuntimeWithUpdateGroup(updateGroups[0], runtime, branchName);
}
async selectRuntimesAsync(
private async selectRuntimesAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
+2 -1
View File
@@ -3,6 +3,7 @@ import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import semver from 'semver';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from './android/UpdatesModule';
@@ -149,7 +150,7 @@ async function ensureEASUpdatesIsConfiguredInExpoConfigAsync({
return {
projectChanged: true,
// TODO(cedric): fix return type of `modifyConfigAsync` to avoid `null` for type === success repsonses
exp: result.config?.expo!,
exp: nullthrows(result.config, 'Expected config to be defined'),
};
case 'warn':
@@ -34,7 +34,7 @@ export async function getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
throw error;
}
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
const { branch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName: channelName,
});
@@ -43,7 +43,7 @@ export async function getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
} = await createChannelOnAppAsync(graphqlClient, {
appId: projectId,
channelName,
branchId,
branchId: branch.id,
});
if (!newChannel) {
@@ -52,7 +52,7 @@ export async function getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
);
}
branchInfo = { branchId, branchName: channelName };
branchInfo = { branchId: branch.id, branchName: channelName };
}
return branchInfo;
+21
View File
@@ -118,6 +118,27 @@ export async function republishAsync({
updateInfoGroup: Object.fromEntries(
updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])
),
fingerprintInfoGroup: Object.fromEntries(
updatesToPublish.map(update => {
const fingerprint = update.fingerprint;
if (!fingerprint) {
return [update.platform, undefined];
}
return [
update.platform,
{
fingerprintHash: fingerprint.hash,
fingerprintSource: fingerprint.source
? {
type: fingerprint.source.type,
bucketKey: fingerprint.source.bucketKey,
isDebugFingerprint: fingerprint.source.isDebugFingerprint,
}
: undefined,
},
];
})
),
};
updatesRepublished = await PublishMutation.publishUpdateGroupAsync(graphqlClient, [
@@ -0,0 +1,119 @@
import Log from '../../log';
import { abridgedDiff } from '../fingerprintDiff';
jest.mock('chalk', () => ({
red: jest.fn(text => `red(${text})`),
green: jest.fn(text => `green(${text})`),
gray: jest.fn(text => `gray(${text})`),
cyan: jest.fn(text => `cyan(${text})`),
}));
jest.spyOn(Log, 'log').mockImplementation(jest.fn());
describe('abridgedDiff', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should output the diff for added and removed lines with context', () => {
const str1 = 'Line1\nLine2\nLine3';
const str2 = 'Line1\nLineX\nLine3';
abridgedDiff(str1, str2, 1);
const expectedOutput = [
'cyan(@@ -2,1 +2,1 @@)',
' gray(Line1)',
'red(-Line2)',
'green(+LineX)',
' gray(Line3)',
].join('\n');
expect(Log.log).toHaveBeenCalledTimes(1);
expect(Log.log).toHaveBeenCalledWith(expectedOutput);
});
it('should output the diff for completely different strings', () => {
const str1 = 'LineA\nLineB';
const str2 = 'LineC\nLineD';
abridgedDiff(str1, str2, 0);
const expectedOutput = [
'cyan(@@ -1,2 +1,2 @@)',
'red(-LineA)',
'red(-LineB)',
'green(+LineC)',
'green(+LineD)',
].join('\n');
expect(Log.log).toHaveBeenCalledTimes(1);
expect(Log.log).toHaveBeenCalledWith(expectedOutput);
});
it('should not include context lines if contextLines is 0', () => {
const str1 = 'Line1\nLine2\nLine3';
const str2 = 'Line1\nLineX\nLine3';
abridgedDiff(str1, str2, 0);
const expectedOutput = ['cyan(@@ -2,1 +2,1 @@)', 'red(-Line2)', 'green(+LineX)'].join('\n');
expect(Log.log).toHaveBeenCalledTimes(1);
expect(Log.log).toHaveBeenCalledWith(expectedOutput);
});
it('should handle strings with no differences', () => {
const str1 = 'SameLine1\nSameLine2';
const str2 = 'SameLine1\nSameLine2';
abridgedDiff(str1, str2, 1);
expect(Log.log).toHaveBeenCalledTimes(1);
expect(Log.log).toHaveBeenCalledWith('');
});
it('should handle empty input strings', () => {
const str1 = '';
const str2 = 'NewLine1\nNewLine2';
abridgedDiff(str1, str2, 1);
const expectedOutput = ['cyan(@@ -1,0 +1,2 @@)', 'green(+NewLine1)', 'green(+NewLine2)'].join(
'\n'
);
expect(Log.log).toHaveBeenCalledTimes(1);
expect(Log.log).toHaveBeenCalledWith(expectedOutput);
});
it('should handle strings with only removed lines', () => {
const str1 = 'OldLine1\nOldLine2';
const str2 = '';
abridgedDiff(str1, str2, 1);
const expectedOutput = ['cyan(@@ -1,2 +1,0 @@)', 'red(-OldLine1)', 'red(-OldLine2)'].join('\n');
expect(Log.log).toHaveBeenCalledTimes(1);
expect(Log.log).toHaveBeenCalledWith(expectedOutput);
});
it('should handle strings with mixed changes and context lines', () => {
const str1 = 'Header\nLine1\nLine2\nFooter';
const str2 = 'Header\nLine1\nLineX\nFooter';
abridgedDiff(str1, str2, 1);
const expectedOutput = [
'cyan(@@ -3,1 +3,1 @@)',
' gray(Line1)',
'red(-Line2)',
'green(+LineX)',
' gray(Footer)',
].join('\n');
expect(Log.log).toHaveBeenCalledTimes(1);
expect(Log.log).toHaveBeenCalledWith(expectedOutput);
});
});
@@ -0,0 +1,14 @@
export default async function mapMapAsync<K, V, M>(
map: ReadonlyMap<K, V>,
mapper: (value: V, key: K) => Promise<M>
): Promise<Map<K, M>> {
const resultingMap: Map<K, M> = new Map();
await Promise.all(
Array.from(map.keys()).map(async k => {
const initialValue = map.get(k) as V;
const result = await mapper(initialValue, k);
resultingMap.set(k, result);
})
);
return resultingMap;
}
+114
View File
@@ -0,0 +1,114 @@
/**
* DO NOT EDIT unless the same change is made in `@expo/fingerprint`
*/
export interface HashSourceFile {
type: 'file';
filePath: string;
/**
* Reasons of this source coming from
*/
reasons: string[];
}
export interface HashSourceDir {
type: 'dir';
filePath: string;
/**
* Reasons of this source coming from
*/
reasons: string[];
}
export interface HashSourceContents {
type: 'contents';
id: string;
contents: string | Buffer;
/**
* Reasons of this source coming from
*/
reasons: string[];
}
export type HashSource = HashSourceFile | HashSourceDir | HashSourceContents;
export interface Fingerprint {
/**
* Sources and their hash values to generate a fingerprint
*/
sources: FingerprintSource[];
/**
* The final hash value of the whole fingerprint
*/
hash: string;
}
export interface DebugInfoFile {
path: string;
hash: string;
}
export interface DebugInfoDir {
path: string;
hash: string;
children: (DebugInfoFile | DebugInfoDir | undefined)[];
}
export interface DebugInfoContents {
hash: string;
}
export type DebugInfo = DebugInfoFile | DebugInfoDir | DebugInfoContents;
export type FingerprintSource = HashSource & {
/**
* Hash value of the `source`.
* If the source is excluding by `Options.dirExcludes`, the value will be null.
*/
hash: string | null;
/**
* Debug info from the hashing process. Differs based on source type. Designed to be consumed by humans
* as opposed to programmatically.
*/
debugInfo?: DebugInfo;
};
export type FingerprintDiffItem =
| {
/**
* The operation type of the diff item.
*/
op: 'added';
/**
* The added source.
*/
addedSource: FingerprintSource;
}
| {
/**
* The operation type of the diff item.
*/
op: 'removed';
/**
* The removed source.
*/
removedSource: FingerprintSource;
}
| {
/**
* The operation type of the diff item.
*/
op: 'changed';
/**
* The source before.
*/
beforeSource: FingerprintSource;
/**
* The source after.
*/
afterSource: FingerprintSource;
};
+120 -24
View File
@@ -1,23 +1,43 @@
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 Log from '../log';
import { ora } from '../ora';
export type FingerprintOptions = {
workflow: Workflow;
platforms: string[];
debug?: boolean;
env: Env | undefined;
cwd?: string;
};
export function diffFingerprint(
projectDir: string,
fingerprint1: Fingerprint,
fingerprint2: Fingerprint
): FingerprintDiffItem[] | null {
// @expo/fingerprint is exported in the expo package for SDK 52+
const fingerprintPath = silentResolveFrom(projectDir, 'expo/fingerprint');
if (!fingerprintPath) {
return null;
}
const Fingerprint = require(fingerprintPath);
return Fingerprint.diffFingerprints(fingerprint1, fingerprint2);
}
export async function createFingerprintAsync(
projectDir: string,
options: {
workflow: Workflow;
platform: string;
debug?: boolean;
env: Env | undefined;
cwd?: string;
}
): Promise<{
hash: string;
sources: object[];
isDebugSource: boolean;
} | null> {
options: FingerprintOptions
): Promise<
| (Fingerprint & {
isDebugSource: boolean;
})
| null
> {
// @expo/fingerprint is exported in the expo package for SDK 52+
const fingerprintPath = silentResolveFrom(projectDir, 'expo/fingerprint');
if (!fingerprintPath) {
@@ -36,18 +56,11 @@ export async function createFingerprintAsync(
const spinner = ora(`Computing project fingerprint`).start();
try {
const Fingerprint = require(fingerprintPath);
const fingerprintOptions: Record<string, any> = {};
if (options.platform) {
fingerprintOptions.platforms = [options.platform];
}
if (options.workflow === Workflow.MANAGED) {
fingerprintOptions.ignorePaths = ['android/**/*', 'ios/**/*'];
}
if (options.debug) {
fingerprintOptions.debug = true;
}
const fingerprint = await Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions);
const fingerprint = await createFingerprintWithoutLoggingAsync(
projectDir,
fingerprintPath,
options
);
spinner.succeed(`Computed project fingerprint`);
return fingerprint;
} catch (e) {
@@ -60,3 +73,86 @@ export async function createFingerprintAsync(
spinner.stop();
}
}
async function createFingerprintWithoutLoggingAsync(
projectDir: string,
fingerprintPath: string,
options: FingerprintOptions
): Promise<
Fingerprint & {
isDebugSource: boolean;
}
> {
const Fingerprint = require(fingerprintPath);
const fingerprintOptions: Record<string, any> = {};
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);
}
/**
* Computes project fingerprints based on provided options and returns a map of fingerprint data keyed by a string.
*
* @param projectDir - The root directory of the project.
* @param fingerprintOptionsByKey - A map where each key is associated with options for generating the fingerprint.
* - **Key**: A unique identifier (`string`) for the fingerprint options.
* - **Value**: An object containing options for generating a fingerprint.
*
* @returns A promise that resolves to a map where each key corresponds to the input keys, and each value is an object containing fingerprint data.
*
* @throws Will throw an error if fingerprint computation fails.
*/
export async function createFingerprintsByKeyAsync(
projectDir: string,
fingerprintOptionsByKey: Map<string, FingerprintOptions>
): Promise<
Map<
string,
Fingerprint & {
isDebugSource: boolean;
}
>
> {
// @expo/fingerprint is exported in the expo package for SDK 52+
const fingerprintPath = silentResolveFrom(projectDir, 'expo/fingerprint');
if (!fingerprintPath) {
return new Map();
}
if (process.env.EAS_SKIP_AUTO_FINGERPRINT) {
Log.log('Skipping project fingerprints');
return new Map();
}
const timeoutId = setTimeout(() => {
Log.log('⌛️ Computing the project fingerprints is taking longer than expected...');
Log.log('⏩ To skip this step, set the environment variable: EAS_SKIP_AUTO_FINGERPRINT=1');
}, 5000);
const spinner = ora(`Computing project fingerprints`).start();
try {
const fingerprintsByKey = await mapMapAsync(
fingerprintOptionsByKey,
async options =>
await createFingerprintWithoutLoggingAsync(projectDir, fingerprintPath, options)
);
spinner.succeed(`Computed project fingerprints`);
return fingerprintsByKey;
} catch (e) {
spinner.fail(`Failed to compute project fingerprints`);
Log.log('⏩ To skip this step, set the environment variable: EAS_SKIP_AUTO_FINGERPRINT=1');
throw e;
} finally {
// Clear the timeout if the operation finishes before the time limit
clearTimeout(timeoutId);
spinner.stop();
}
}
@@ -0,0 +1,127 @@
import chalk from 'chalk';
import { diffLines } from 'diff';
import Log from '../log';
/**
* Computes and prints a line-based diff between two strings, displaying changes as chunks.
*
* Each chunk contains lines that were added or removed, prefixed with contextual lines
* from the unchanged parts of the strings. The output is styled similarly to `git diff`
* with headers indicating line ranges for the changes in the original and modified strings.
*
* @param {string} str1 - The original string to compare.
* @param {string} str2 - The modified string to compare against the original.
* @param {number} [contextLines=2] - The number of unchanged lines to display before and after each chunk of changes.
*
* ### Output:
* - Each chunk begins with a header in the format `@@ -<original range> +<modified range> @@`.
* - Removed lines are prefixed with a red `-` and the original line number.
* - Added lines are prefixed with a green `+` and the modified line number.
* - Context lines are displayed in gray without a `+` or `-` prefix.
*
* ### Notes:
* - Consecutive changes are grouped into a single chunk if separated by no more than the specified number of context lines.
*
* ### Example:
* ```typescript
* abridgedDiff("Line1\nLine2\nLine3", "Line1\nLineX\nLine3", 1);
*
* Output:
* `@@ -2,1 +2,1 @@`
* Line1
* -Line2
* +LineX
* Line3
* ```
*/
export function abridgedDiff(str1: string, str2: string, contextLines: number = 2): void {
const changes = diffLines(str1, str2);
const output: string[] = [];
let lineNumberOriginal = 1;
let lineNumberModified = 1;
let currentChunk: string[] = [];
let currentChunkPriorContext: string[] = [];
let currentChunkAfterContext: string[] = [];
let startOriginal: number | null = null; // Start line in the original for the current chunk
let startModified: number | null = null; // Start line in the modified for the current chunk
let addedLines = 0;
let removedLines = 0;
const flushChunk = (): void => {
if (currentChunk.length > 0) {
const originalRange = `${startOriginal},${removedLines || 0}`;
const modifiedRange = `${startModified},${addedLines || 0}`;
// `git diff` style header
output.push(chalk.cyan(`@@ -${originalRange} +${modifiedRange} @@`));
output.push(...currentChunkPriorContext);
output.push(...currentChunk);
output.push(...currentChunkAfterContext);
currentChunk = [];
currentChunkPriorContext = [];
currentChunkAfterContext = [];
addedLines = 0;
removedLines = 0;
}
};
for (const change of changes) {
const lines = change.value.split('\n').filter(line => line);
if (change.added || change.removed) {
// Initialize start lines for the chunk if not already set
if (startOriginal === null) {
startOriginal = lineNumberOriginal;
}
if (startModified === null) {
startModified = lineNumberModified;
}
if (change.removed) {
lines.forEach(line => {
currentChunk.push(`${chalk.red(`-${line}`)}`);
lineNumberOriginal++;
removedLines++;
});
}
if (change.added) {
lines.forEach(line => {
currentChunk.push(`${chalk.green(`+${line}`)}`);
lineNumberModified++;
addedLines++;
});
}
} else {
// Unchanged lines (context)
lines.forEach((line, i) => {
if (currentChunk.length > 0) {
// Add leading context after a change
if (i < contextLines) {
currentChunkAfterContext.push(` ${chalk.gray(line)}`);
}
} else {
// Add trailing context before a change
if (lines.length - 1 - contextLines < i) {
currentChunkPriorContext.push(` ${chalk.gray(line)}`);
}
}
const isFinalLineOfAfterContext = i === contextLines - 1 || i === lines.length - 1;
if (currentChunk.length > 0 && isFinalLineOfAfterContext) {
flushChunk();
}
lineNumberOriginal++;
lineNumberModified++;
});
startOriginal = null;
startModified = null;
}
}
flushChunk(); // Flush any remaining chunk
Log.log(output.join('\n'));
}
+2 -2
View File
@@ -32,14 +32,14 @@ export async function promptVariableTypeAsync(
}
export function parseVisibility(
stringVisibility: 'plaintext' | 'sensitive' | 'encrypted'
stringVisibility: 'plaintext' | 'sensitive' | 'secret'
): EnvironmentVariableVisibility {
switch (stringVisibility) {
case 'plaintext':
return EnvironmentVariableVisibility.Public;
case 'sensitive':
return EnvironmentVariableVisibility.Sensitive;
case 'encrypted':
case 'secret':
return EnvironmentVariableVisibility.Secret;
default:
throw new Error(`Invalid visibility: ${stringVisibility}`);
@@ -42,13 +42,13 @@ function warnAboutServiceOutage(service: StatuspageServiceFragment): void {
if (service.incidents.length > 0) {
const [currentIncident] = service.incidents;
Log.warn(`Reason: ${currentIncident.name}`);
Log.warn(`Reason: ${currentIncident.name}.`);
}
Log.warn(
`All information on service status and incidents available at ${link(
'https://status.expo.dev/'
)}.`
)}`
);
Log.newLine();
}
+1 -1
View File
@@ -60,7 +60,7 @@ export default class GitClient extends Client {
Log.warn('EAS requires you to use a git repository for your project.');
const cwd = process.cwd();
const repoRoot = PackageManagerUtils.findWorkspaceRoot(cwd) ?? cwd;
const repoRoot = PackageManagerUtils.resolveWorkspaceRoot(cwd) ?? cwd;
const confirmInit = await confirmAsync({
message: `Would you like us to run 'git init' in ${
this.maybeCwdOverride ?? repoRoot
+29 -15
View File
@@ -1,4 +1,4 @@
import { get as getEnv } from '@expo/env';
import { parseProjectEnv } from '@expo/env';
import { Gzip, GzipOptions } from 'minizlib';
import { HashOptions, createHash, randomBytes } from 'node:crypto';
import fs, { createWriteStream } from 'node:fs';
@@ -98,6 +98,11 @@ export interface Manifest {
env: Record<string, string | undefined>;
}
export interface CreateManifestResult {
conflictingVariableNames: string[] | undefined;
manifest: Manifest;
}
interface CreateManifestParams {
projectId: string;
projectDir: string;
@@ -108,23 +113,32 @@ interface CreateManifestParams {
export async function createManifestAsync(
params: CreateManifestParams,
graphqlClient: ExpoGraphqlClient
): Promise<Manifest> {
let env: Record<string, string | undefined>;
): Promise<CreateManifestResult> {
// Resolve .env file variables
const { env } = parseProjectEnv(params.projectDir, { mode: 'production' });
// Maybe load EAS Environment Variables (based on `--environment` arg)
let conflictingVariableNames: string[] | undefined;
if (params.environment) {
env = Object.fromEntries(
(
await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: params.projectId,
environment: params.environment,
})
).map(variable => [variable.name, variable.value ?? undefined])
const loadedVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
{
appId: params.projectId,
environment: params.environment,
}
);
} else {
// NOTE: This is required for the .env resolution
process.env.NODE_ENV = 'production';
env = getEnv(params.projectDir).env;
// Load EAS Env vars into `env` object, keeping track of conflicts
conflictingVariableNames = [];
for (const variable of loadedVariables) {
if (variable.value != null) {
if (env[variable.name] != null) {
conflictingVariableNames.push(variable.name);
}
env[variable.name] = variable.value;
}
}
}
return { env };
const manifest: Manifest = { env };
return { conflictingVariableNames, manifest };
}
interface WorkerFileEntry {
+7 -4
View File
@@ -109,20 +109,23 @@ export async function uploadAsync(params: UploadParams): Promise<UploadResult> {
return retry(error);
}
const getErrorMessageAsync = async (): Promise<string> => {
const body = await response.json().catch(() => null);
return body?.error ?? `Upload of "${filePath}" failed: ${response.statusText}`;
};
if (
response.status === 408 ||
response.status === 409 ||
response.status === 429 ||
(response.status >= 500 && response.status <= 599)
) {
const message = `Upload of "${filePath}" failed: ${response.statusText}`;
const text = await response.text().catch(() => null);
return retry(new Error(text ? `${message}\n${text}` : message));
return retry(new Error(await getErrorMessageAsync()));
} else if (response.status === 413) {
const message = `Upload of "${filePath}" failed: File size exceeded the upload limit`;
throw new Error(message);
} else if (!response.ok) {
throw new Error(`Upload of "${filePath}" failed: ${response.statusText}`);
throw new Error(await getErrorMessageAsync());
}
return {
+2 -3
View File
@@ -1,16 +1,15 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"esModuleInterop": true,
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"target": "ES2021",
"outDir": "build",
"rootDir": "src",
"typeRoots": ["../../ts-declarations", "../../node_modules/@types", "./node_modules/@types"]
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more