Compare commits

...
Author SHA1 Message Date
Kudo Chien e7b7eacd4e fix lint errors 2024-12-19 00:20:26 +08:00
Kudo Chien ec43906005 update changelog 2024-12-19 00:19:03 +08:00
Kudo Chien b71f324c83 [eas-cli] support platform version 2024-12-19 00:11:52 +08: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
Stanisław Chmiela da727ba170 v13.3.0 2024-11-18 22:35:12 +01:00
4571910c4d Adds workflow:create command (#2678)
# Why

Adds `eas workflow:create` to make it faster to create workflow files in a project. This command creates a file like **.eas/workflows/workflow.yml**. The user can choose the file name or can provide it as an arugment.

# Screenshots

<img width="552" alt="Screenshot 2024-11-08 at 8 50 58 AM" src="https://github.com/user-attachments/assets/5cf82e7e-cce6-4098-b291-fccdac25f103">
<img width="584" alt="Screenshot 2024-11-08 at 8 51 27 AM" src="https://github.com/user-attachments/assets/d6f6e7cb-18c1-420e-b52a-5bdd42446715">
<img width="617" alt="Screenshot 2024-11-08 at 8 52 04 AM" src="https://github.com/user-attachments/assets/81cc79cf-afb7-4855-bfce-0679f98655fa">

# Test Plan

Make sure that you can run the command as pictured above and that it correctly creates the workflow file. Also make sure that the unit tests pass.

# Future

Once we have docs for workflows that are public, I'd like to put a link to the docs in the default file this command creates. For now, I am just putting Hello World as a reasonable placeholder.

Co-authored-by: Szymon Dziedzic <[email protected]>
Co-authored-by: Stanisław Chmiela <[email protected]>
2024-11-18 22:34:39 +01:00
Stanisław Chmiela c7cfcafff8 Fix changelog (#2701) 2024-11-18 22:29:41 +01:00
Stanisław Chmiela 6f913f7320 Update CHANGELOG.md 2024-11-18 19:33:54 +01:00
Stanisław Chmiela 0a49296231 [eas-cli] Unhide workflow commands (#2683)
<!-- 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

Going to release soon.

# How

Removed `hidden = true` overrides.

# Test Plan

`easd --help` printed out information about workflow commands.

```
~/Developer/test-local (main [$!]) easd --help
EAS command line tool

VERSION
  eas-cli/13.1.1 darwin-arm64 node-v22.4.0

USAGE
  $ eas [COMMAND]

TOPICS
...
  workflow     manage workflows
```

```
~/Developer/test-local (main [$!]) easd workflow --help                       
manage workflows

USAGE
  $ eas workflow:COMMAND

COMMANDS
  workflow:run       Run an EAS workflow
  workflow:validate  validate a workflow configuration yaml file
```
2024-11-18 19:28:49 +01:00
Stanisław Chmiela 7fc09b2376 [eas-cli] Improve workflow:run and workflow:validate (#2700)
<!-- 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

- running workflows requires GitHub connection
- workflow config may be a valid YAML, but invalid config

We want to check both of these:
- in `workflow:validate`
- before we upload project sources to GCS in `workflow:run`.

# How

I added a `validateWorkflowRevisionYamlConfig` mutation I'm using here to validate the config _and_ GitHub connection. The GitHub requirement is checked dynamically in `www` so we can later remove it without having users upgrade to a newer `eas-cli`.

Brent also wished `eas workflow:run test.yml` resolved `test.yml` to `.eas/workflows/test.yml` automatically, so added this too.

# Test Plan

I tried validating and running invalid YAMLs, workflows and valid configs. Things seemed to work ok.
2024-11-18 19:26:45 +01:00
Expo CI fed28a87be update CHANGELOG.md 2024-11-15 11:03:36 +00:00
Szymon Dziedzic 073bdcd575 v13.2.3 2024-11-15 10:57:48 +00:00
Szymon Dziedzic 6979c8d67b [eas-cli] make logs anouncing loading env variables less noisy (#2694) 2024-11-15 11:51:41 +01:00
Szymon Dziedzic 0869bc511f [website] rename encrypted visibility type to secret (#2693) 2024-11-15 11:50:24 +01:00
Expo CI 693562d742 update CHANGELOG.md 2024-11-15 10:35:32 +00:00
Szymon Dziedzic 734a071345 v13.2.2 2024-11-15 10:30:04 +00:00
Szymon Dziedzic 26fd7a7daa [ENG-14180][eas-cli] rename with-as-environment-variables-set eas update flag to environment (#2692)
<!-- 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

During dogfooding it came out that this flag is too long and confusing

# How

Just use `environment`

# Test Plan

Tests, types
2024-11-15 11:28:50 +01:00
Expo Bot df13e0433e Upgrade eas-build dependencies (#2691)
⬆️ Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.
2024-11-14 22:14:45 +01:00
Expo CI fa72248d45 update CHANGELOG.md 2024-11-14 15:54:43 +00:00
Stanisław Chmiela 2b51db72db v13.2.1 2024-11-14 16:46:12 +01:00
Stanisław Chmiela 79696394be [eas-cli] Default update message to undefined in no-VCS client (#2690)
<!-- 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://staging.expo.dev/accounts/sjchmiela/projects/staging-app/workflows/01932b13-21af-780b-b531-1ed07e3d8228

`eas update --auto` fails in GCS update job because this flag is set to true.

# How

The comment says it can be `false`, so changed it to `false`.

https://github.com/expo/eas-cli/pull/2148/files#diff-a594e233350755033d45aa7b21b78781e4b36322e9312f60e71e7edc79c33a7f

# Test Plan

Update message should be changed to `undefined`.
2024-11-14 16:40:59 +01:00
Expo CI 86c99efdcb update CHANGELOG.md 2024-11-13 15:08:46 +00:00
105 changed files with 4327 additions and 1593 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']
+117 -2
View File
@@ -8,13 +8,128 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add EAS_SKIP_AUTO_FINGERPRINT to skip fingerprint computation on build ([#2675](https://github.com/expo/eas-cli/pull/2675) by [@quinlanj](https://github.com/quinlanj))
- Default build-logger-level to debug when EXPO_DEBUG is set. ([#2676](https://github.com/expo/eas-cli/pull/2676) by [@wschurman](https://github.com/wschurman))
- Added support to read platform version from `app.json`. ([#2778](https://github.com/expo/eas-cli/pull/2778) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Show `eas deploy` upload error messages. ([#2771](https://github.com/expo/eas-cli/pull/2771) by [@kadikraman](https://github.com/kadikraman))
### 🧹 Chores
## [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))
### 🐛 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
## [13.2.1](https://github.com/expo/eas-cli/releases/tag/v13.2.1) - 2024-11-14
## [13.2.0](https://github.com/expo/eas-cli/releases/tag/v13.2.0) - 2024-11-13
### 🎉 New features
- Add EAS_SKIP_AUTO_FINGERPRINT to skip fingerprint computation on build ([#2675](https://github.com/expo/eas-cli/pull/2675) by [@quinlanj](https://github.com/quinlanj))
- Default build-logger-level to debug when EXPO_DEBUG is set. ([#2676](https://github.com/expo/eas-cli/pull/2676) by [@wschurman](https://github.com/wschurman))
## [13.1.1](https://github.com/expo/eas-cli/releases/tag/v13.1.1) - 2024-11-08
## [13.1.0](https://github.com/expo/eas-cli/releases/tag/v13.1.0) - 2024-11-07
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "13.2.0",
"version": "14.2.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+362 -165
View File
@@ -94,6 +94,14 @@ eas --help COMMAND
* [`eas device:rename`](#eas-devicerename)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas env:create [ENVIRONMENT]`](#eas-envcreate-environment)
* [`eas env:delete [ENVIRONMENT]`](#eas-envdelete-environment)
* [`eas env:exec ENVIRONMENT BASH_COMMAND`](#eas-envexec-environment-bash_command)
* [`eas env:get [ENVIRONMENT]`](#eas-envget-environment)
* [`eas env:list [ENVIRONMENT]`](#eas-envlist-environment)
* [`eas env:pull [ENVIRONMENT]`](#eas-envpull-environment)
* [`eas env:push [ENVIRONMENT]`](#eas-envpush-environment)
* [`eas env:update [ENVIRONMENT]`](#eas-envupdate-environment)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory)
@@ -107,10 +115,6 @@ eas --help COMMAND
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas project:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-projectonboarding-target_project_directory)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas secret:push`](#eas-secretpush)
* [`eas submit`](#eas-submit)
* [`eas update`](#eas-update)
* [`eas update:configure`](#eas-updateconfigure)
@@ -127,6 +131,9 @@ eas --help COMMAND
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
* [`eas whoami`](#eas-whoami)
* [`eas workflow:create [NAME]`](#eas-workflowcreate-name)
* [`eas workflow:run [FILE]`](#eas-workflowrun-file)
* [`eas workflow:validate PATH`](#eas-workflowvalidate-path)
## `eas account:login`
@@ -146,7 +153,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -163,7 +170,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -180,7 +187,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -194,7 +201,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -246,7 +253,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -267,7 +274,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -287,7 +294,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -307,7 +314,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -330,7 +337,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -367,7 +374,7 @@ DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -386,7 +393,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -403,7 +410,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -422,7 +429,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:inspect`
@@ -457,7 +464,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -507,7 +514,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -536,7 +543,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -562,7 +569,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -612,7 +619,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -631,7 +638,7 @@ DESCRIPTION
update version of an app
```
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -650,7 +657,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -667,7 +674,7 @@ DESCRIPTION
view a build for your project
```
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -688,7 +695,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -710,7 +717,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -730,7 +737,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -752,7 +759,7 @@ DESCRIPTION
pause a channel to stop it from sending updates
```
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/channel/pause.ts)_
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -774,7 +781,7 @@ DESCRIPTION
resume a channel to start sending updates
```
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/channel/resume.ts)_
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -807,7 +814,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -830,7 +837,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -851,7 +858,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -868,7 +875,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -886,7 +893,7 @@ DESCRIPTION
Set up credentials for building your project.
```
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas device:create`
@@ -900,7 +907,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -920,7 +927,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -941,7 +948,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -962,7 +969,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -976,7 +983,7 @@ DESCRIPTION
view a device for your project
```
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -990,7 +997,206 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
create an environment variable for the current project or account
```
USAGE
$ eas env:create [ENVIRONMENT] [--name <value>] [--value <value>] [--force] [--type string|file] [--visibility
plaintext|sensitive|secret] [--scope project|account] [--environment development|preview|production]
[--non-interactive]
ARGUMENTS
ENVIRONMENT Environment to create the variable in. One of 'production', 'preview', or 'development'.
FLAGS
--environment=(development|preview|production)... Environment variable's environment
--force Overwrite existing variable
--name=<value> Name of the variable
--non-interactive Run the command in non-interactive mode.
--scope=(project|account) [default: project] Scope for the variable
--type=(string|file) The type of variable
--value=<value> Text value or the variable
--visibility=(plaintext|sensitive|secret) Visibility of the variable
DESCRIPTION
create an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
delete an environment variable for the current project or account
```
USAGE
$ eas env:delete [ENVIRONMENT] [--variable-name <value>] [--variable-environment
development|preview|production] [--scope project|account] [--non-interactive]
ARGUMENTS
ENVIRONMENT Current environment of the variable to delete. One of 'production', 'preview', or 'development'.
FLAGS
--non-interactive Run the command in non-interactive mode.
--scope=(project|account) [default: project] Scope for the variable
--variable-environment=(development|preview|production) Current environment of the variable to delete
--variable-name=<value> Name of the variable to delete
DESCRIPTION
delete an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
execute a command with environment variables from the selected environment
```
USAGE
$ eas env:exec ENVIRONMENT BASH_COMMAND [--non-interactive]
ARGUMENTS
ENVIRONMENT Environment to execute the command in. One of 'production', 'preview', or 'development'.
BASH_COMMAND bash command to execute with the environment variables from the environment
FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
execute a command with environment variables from the selected environment
```
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
view an environment variable for the current project or account
```
USAGE
$ eas env:get [ENVIRONMENT] [--variable-name <value>] [--variable-environment
development|preview|production] [--format long|short] [--scope project|account] [--non-interactive]
ARGUMENTS
ENVIRONMENT Current environment of the variable. One of 'production', 'preview', or 'development'.
FLAGS
--format=(long|short) [default: short] Output format
--non-interactive Run the command in non-interactive mode.
--scope=(project|account) [default: project] Scope for the variable
--variable-environment=(development|preview|production) Current environment of the variable
--variable-name=<value> Name of the variable
DESCRIPTION
view an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
list environment variables for the current project or account
```
USAGE
$ eas env:list [ENVIRONMENT] [--include-sensitive] [--include-file-content] [--environment
development|preview|production] [--format long|short] [--scope project|account]
ARGUMENTS
ENVIRONMENT Environment to list the variables from. One of 'production', 'preview', or 'development'.
FLAGS
--environment=(development|preview|production)... Environment variable's environment
--format=(long|short) [default: short] Output format
--include-file-content Display files content in the output
--include-sensitive Display sensitive values in the output
--scope=(project|account) [default: project] Scope for the variable
DESCRIPTION
list environment variables for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
pull environment variables for the selected environment to .env file
```
USAGE
$ eas env:pull [ENVIRONMENT] [--non-interactive] [--environment development|preview|production] [--path
<value>]
ARGUMENTS
ENVIRONMENT Environment to pull variables from. One of 'production', 'preview', or 'development'.
FLAGS
--environment=(development|preview|production) Environment variable's environment
--non-interactive Run the command in non-interactive mode.
--path=<value> [default: .env.local] Path to the result `.env` file
DESCRIPTION
pull environment variables for the selected environment to .env file
```
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
push environment variables from .env file to the selected environment
```
USAGE
$ eas env:push [ENVIRONMENT] [--environment development|preview|production] [--path <value>]
ARGUMENTS
ENVIRONMENT Environment to push variables to. One of 'production', 'preview', or 'development'.
FLAGS
--environment=(development|preview|production)... Environment variable's environment
--path=<value> [default: .env.local] Path to the input `.env` file
DESCRIPTION
push environment variables from .env file to the selected environment
```
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
update an environment variable on the current project or account
```
USAGE
$ eas env:update [ENVIRONMENT] [--variable-name <value>] [--variable-environment
development|preview|production] [--name <value>] [--value <value>] [--type string|file] [--visibility
plaintext|sensitive|secret] [--scope project|account] [--environment development|preview|production]
[--non-interactive]
ARGUMENTS
ENVIRONMENT Current environment of the variable to update. One of 'production', 'preview', or 'development'.
FLAGS
--environment=(development|preview|production)... Environment variable's environment
--name=<value> New name of the variable
--non-interactive Run the command in non-interactive mode.
--scope=(project|account) [default: project] Scope for the variable
--type=(string|file) The type of variable
--value=<value> New value or the variable
--variable-environment=(development|preview|production) Current environment of the variable to update
--variable-name=<value> Current name of the variable
--visibility=(plaintext|sensitive|secret) Visibility of the variable
DESCRIPTION
update an environment variable on the current project or account
```
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/env/update.ts)_
## `eas help [COMMAND]`
@@ -1098,7 +1304,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1115,7 +1321,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1132,7 +1338,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1162,7 +1368,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1176,7 +1382,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1199,7 +1405,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1217,82 +1423,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas secret:create`
create an environment secret on the current project or owner account
```
USAGE
$ eas secret:create [--scope account|project] [--name <value>] [--value <value>] [--type string|file] [--force]
[--non-interactive]
FLAGS
--force Delete and recreate existing secrets
--name=<value> Name of the secret
--non-interactive Run the command in non-interactive mode.
--scope=(account|project) [default: project] Scope for the secret
--type=(string|file) The type of secret
--value=<value> Text value or path to a file to store in the secret
DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [packages/eas-cli/src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
delete an environment secret by ID
```
USAGE
$ eas secret:delete [--id <value>] [--non-interactive]
FLAGS
--id=<value> ID of the secret to delete
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
delete an environment secret by ID
```
_See code: [packages/eas-cli/src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
list environment secrets available for your current app
```
USAGE
$ eas secret:list
DESCRIPTION
list environment secrets available for your current app
```
_See code: [packages/eas-cli/src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
read environment secrets from env file and store on the server
```
USAGE
$ eas secret:push [--scope account|project] [--env-file <value>] [--force] [--non-interactive]
FLAGS
--env-file=<value> Env file with secrets
--force Delete and recreate existing secrets
--non-interactive Run the command in non-interactive mode.
--scope=(account|project) [default: project] Scope for the secrets
DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [packages/eas-cli/src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -1323,7 +1454,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1333,35 +1464,39 @@ publish an update group
USAGE
$ eas update [--branch <value>] [--channel <value>] [-m <value>] [--input-dir <value>] [--skip-bundler]
[--clear-cache] [--emit-metadata] [--rollout-percentage <value>] [-p android|ios|all] [--auto] [--private-key-path
<value>] [--json --non-interactive]
<value>] [--environment development|preview|production] [--json --non-interactive]
FLAGS
-m, --message=<value> A short message describing the update
-p, --platform=(android|ios|all) [default: all]
--auto Use the current git branch and commit message for the EAS branch and update message
--branch=<value> Branch to publish the update group on
--channel=<value> Channel that the published update should affect
--clear-cache Clear the bundler cache before publishing
--emit-metadata Emit "eas-update-metadata.json" in the bundle folder with detailed information about
the generated updates
--input-dir=<value> [default: dist] Location of the bundle
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--private-key-path=<value> 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/
--rollout-percentage=<value> Percentage of users this update should be immediately available to. Users not in the
rollout will be served the previous latest update on the branch, even if that update
is itself being rolled out. The specified number must be an integer between 1 and
100. When not specified, this defaults to 100.
--skip-bundler Skip running Expo CLI to bundle the app before publishing
-m, --message=<value> A short message describing the update
-p, --platform=(android|ios|all) [default: all]
--auto Use the current git branch and commit message for the EAS branch and
update message
--branch=<value> Branch to publish the update group on
--channel=<value> Channel that the published update should affect
--clear-cache Clear the bundler cache before publishing
--emit-metadata Emit "eas-update-metadata.json" in the bundle folder with detailed
information about the generated updates
--environment=(development|preview|production) Environment to use for the server-side defined EAS environment
variables during command execution.
--input-dir=<value> [default: dist] Location of the bundle
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--private-key-path=<value> 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/
--rollout-percentage=<value> Percentage of users this update should be immediately available to.
Users not in the rollout will be served the previous latest update on
the branch, even if that update is itself being rolled out. The
specified number must be an integer between 1 and 100. When not
specified, this defaults to 100.
--skip-bundler Skip running Expo CLI to bundle the app before publishing
DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1369,17 +1504,19 @@ configure the project to support EAS Update
```
USAGE
$ eas update:configure [-p android|ios|all] [--non-interactive]
$ eas update:configure [-p android|ios|all] [--environment development|preview|production] [--non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all] Platform to configure
--non-interactive Run the command in non-interactive mode.
-p, --platform=(android|ios|all) [default: all] Platform to configure
--environment=(development|preview|production) Environment to use for the server-side defined EAS environment
variables during command execution.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1400,7 +1537,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -1424,7 +1561,7 @@ DESCRIPTION
edit all the updates in an update group
```
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/edit.ts)_
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:list`
@@ -1446,7 +1583,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1478,7 +1615,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
@@ -1486,12 +1623,11 @@ roll back to the embedded update
```
USAGE
$ eas update:roll-back-to-embedded [--branch <value>] [--channel <value>] [--message <value>] [-p android|ios|all] [--auto]
[--private-key-path <value>] [--json --non-interactive]
$ eas update:roll-back-to-embedded [--branch <value>] [--channel <value>] [--runtime-version <value>] [--message <value>] [-p
android|ios|all] [--private-key-path <value>] [--json --non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all]
--auto Use the current git branch and commit message for the EAS branch and update message
--branch=<value> Branch to publish the rollback to embedded update group on
--channel=<value> Channel that the published rollback to embedded update should affect
--json Enable JSON output, non-JSON messages will be printed to stderr.
@@ -1501,12 +1637,13 @@ FLAGS
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory. Only relevant if you are using code signing:
https://docs.expo.dev/eas-update/code-signing/
--runtime-version=<value> Runtime version that the rollback to embedded update should target
DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1526,7 +1663,7 @@ DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1546,7 +1683,7 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1567,7 +1704,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1587,7 +1724,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1605,7 +1742,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1627,7 +1764,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1644,7 +1781,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v13.2.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -1660,4 +1797,64 @@ DESCRIPTION
ALIASES
$ eas whoami
```
## `eas workflow:create [NAME]`
create a new workflow configuration YAML file
```
USAGE
$ eas workflow:create [NAME] [--non-interactive]
ARGUMENTS
NAME Name of the workflow file (must end with .yml or .yaml)
FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
create a new workflow configuration YAML file
```
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:run [FILE]`
Run an EAS workflow
```
USAGE
$ eas workflow:run [FILE] [--non-interactive]
ARGUMENTS
FILE Path to the workflow file to run
FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
Run an EAS workflow
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:validate PATH`
validate a workflow configuration yaml file
```
USAGE
$ eas workflow:validate PATH [--non-interactive]
ARGUMENTS
PATH Path to the workflow configuration YAML file (must end with .yml or .yaml)
FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v14.2.0/packages/eas-cli/src/commands/workflow/validate.ts)_
<!-- commandsstop -->
+769 -258
View File
File diff suppressed because it is too large. Load diff
+24 -16
View File
@@ -1,34 +1,34 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "13.2.0",
"version": "14.2.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.144",
"@expo/eas-json": "13.0.0",
"@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.2.0",
"@expo/env": "^0.4.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.6.1",
"@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.144",
"@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",
@@ -96,6 +97,7 @@
"@graphql-codegen/typescript-operations": "4.0.1",
"@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,16 +179,22 @@
"secret": {
"description": "manage project and account secrets"
},
"env": {
"description": "manage project and account environment variables"
},
"update": {
"description": "manage individual updates"
},
"webhook": {
"description": "manage webhooks"
},
"workflow": {
"description": "manage workflows"
}
},
"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 {
+15 -6
View File
@@ -20,7 +20,11 @@ import { getNextVersionCode } from '../../project/android/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
import {
bumpAppVersionAsync,
ensureStaticConfigExists,
getVersionConfigTarget,
} from '../utils/version';
export enum BumpStrategy {
APP_VERSION,
@@ -53,9 +57,11 @@ export async function bumpVersionAsync({
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.ANDROID });
const version = versionGetter(exp);
await updateNativeVersionsAsync({
projectDir,
version: exp.version,
version,
versionCode: exp.android?.versionCode,
});
Log.log('Synchronized versions with build gradle');
@@ -79,7 +85,7 @@ export async function bumpVersionInAppJsonAsync({
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
await bumpAppVersionAsync({ appVersion, projectDir, exp });
await bumpAppVersionAsync({ appVersion, projectDir, exp, platform: Platform.ANDROID });
} else {
const versionCode = AndroidConfig.Version.getVersionCode(exp);
const bumpedVersionCode = getNextVersionCode(versionCode);
@@ -118,9 +124,11 @@ export async function maybeResolveVersionsAsync(
return {};
}
} else {
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.ANDROID });
const appVersion = versionGetter(exp);
return {
appBuildVersion: String(AndroidConfig.Version.getVersionCode(exp)),
appVersion: exp.version,
appVersion,
};
}
}
@@ -202,6 +210,7 @@ export async function resolveRemoteVersionCodeAsync(
applicationId
);
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.ANDROID });
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
@@ -230,7 +239,7 @@ export async function resolveRemoteVersionCodeAsync(
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
storeVersion: localVersions.appVersion ?? versionGetter(exp) ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
@@ -254,7 +263,7 @@ export async function resolveRemoteVersionCodeAsync(
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
storeVersion: localVersions.appVersion ?? versionGetter(exp) ?? '1.0.0',
buildVersion: String(nextBuildVersion),
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
+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
@@ -83,24 +84,13 @@ async function resolveEnvVarsAsync({
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables 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 servers: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables found for the "${environment.toLowerCase()}" environment on EAS servers.`
);
}
const encryptedEnvVars = environmentVariables.filter(({ name, value }) => name && !value);
if (encryptedEnvVars.length > 0) {
Log.warn(
`Some environment variables defined in the "${environment.toLowerCase()}" environment on EAS servers are of "encrypted" type and cannot be read outside of the EAS servers (including EAS CLI): ${encryptedEnvVars
.map(({ name }) => name)
.join(
', '
)}. However, they will be available during the build process happening on the EAS servers. This can lead to potential configuration mismatches between the local development environment and the build environment if the encrypted environment variables are used to resolve the app config.`
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
);
}
@@ -110,10 +100,6 @@ async function resolveEnvVarsAsync({
buildProfile.env && Object.keys(buildProfile.env).join(', ')
}.`
);
} else {
Log.log(
`No environment variables specified in the "${buildProfileName}" build profile "env" configuration.`
);
}
if (
@@ -145,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', () => {
+17 -9
View File
@@ -21,7 +21,11 @@ import uniqBy from '../../utils/expodash/uniqBy';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
import {
bumpAppVersionAsync,
ensureStaticConfigExists,
getVersionConfigTarget,
} from '../utils/version';
const SHORT_VERSION_REGEX = /^\d+(\.\d+){0,2}$/;
@@ -48,9 +52,11 @@ export async function bumpVersionAsync({
ensureStaticConfigExists(projectDir);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.IOS });
const version = versionGetter(exp);
await updateNativeVersionsAsync({
projectDir,
version: exp.version,
version,
buildNumber: exp.ios?.buildNumber,
targets,
});
@@ -73,7 +79,7 @@ export async function bumpVersionInAppJsonAsync({
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.APP_VERSION) {
const appVersion = IOSConfig.Version.getVersion(exp);
await bumpAppVersionAsync({ appVersion, projectDir, exp });
await bumpAppVersionAsync({ appVersion, projectDir, exp, platform: Platform.IOS });
} else {
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
if (isValidBuildNumber(buildNumber)) {
@@ -109,14 +115,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'
)}`
);
@@ -141,8 +147,10 @@ export async function readShortVersionAsync(
validateShortVersion({ shortVersion, workflow });
return shortVersion;
} else {
validateShortVersion({ shortVersion: exp.version, workflow });
return exp.version;
const { versionGetter } = getVersionConfigTarget({ exp, platform: Platform.IOS });
const shortVersion = versionGetter(exp);
validateShortVersion({ shortVersion, workflow });
return shortVersion;
}
}
+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 {
/**
@@ -0,0 +1,148 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { updateAppJsonConfigAsync } from '../appJson';
import { bumpAppVersionAsync, getVersionConfigTarget } from '../version';
jest.mock('../appJson', () => ({
__esModule: true,
updateAppJsonConfigAsync: jest.fn().mockImplementation(
async (
{
exp,
}: {
projectDir: string;
exp: ExpoConfig;
},
modifyConfig: (config: any) => void
) => {
// a mocked implementation that only mutates the config object without writing to disk
modifyConfig(exp);
}
),
}));
describe(bumpAppVersionAsync, () => {
const name = 'test';
const slug = 'test';
const projectDir = '/app';
const mockUpdateAppJsonConfigAsync = updateAppJsonConfigAsync as jest.MockedFunction<
typeof updateAppJsonConfigAsync
>;
it('should bump expo.version for valid semver', async () => {
const appVersion = '1.0.0';
const exp: ExpoConfig = {
name,
slug,
version: appVersion,
};
await bumpAppVersionAsync({ appVersion, projectDir, exp, platform: Platform.IOS });
expect(mockUpdateAppJsonConfigAsync).toHaveBeenCalled();
expect(exp.version).toBe('1.0.1');
});
it('should bump expo.android.version if the expo.android.version exists', async () => {
const exp: ExpoConfig = {
name,
slug,
version: '0.0.0',
android: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '1.0.0',
},
ios: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '2.0.0',
},
};
await bumpAppVersionAsync({ appVersion: '1.0.0', projectDir, exp, platform: Platform.ANDROID });
expect(mockUpdateAppJsonConfigAsync).toHaveBeenCalled();
expect(exp.version).toBe('0.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.android.version).toBe('1.0.1');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.ios.version).toBe('2.0.0');
});
it('should bump expo.ios.version if the expo.ios.version exists', async () => {
const exp: ExpoConfig = {
name,
slug,
version: '0.0.0',
android: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '1.0.0',
},
ios: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '2.0.0',
},
};
await bumpAppVersionAsync({ appVersion: '2.0.0', projectDir, exp, platform: Platform.IOS });
expect(mockUpdateAppJsonConfigAsync).toHaveBeenCalled();
expect(exp.version).toBe('0.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.android.version).toBe('1.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(exp.ios.version).toBe('2.0.1');
});
});
describe(getVersionConfigTarget, () => {
const exp: ExpoConfig = {
name: 'test',
slug: 'test',
version: '0.0.0',
android: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '1.0.0',
},
ios: {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
version: '2.0.0',
},
};
it('should return the correct config target for the android platform', () => {
const { fieldName, versionGetter, versionUpdater } = getVersionConfigTarget({
exp,
platform: Platform.ANDROID,
});
expect(fieldName).toBe('expo.android.version');
expect(versionGetter(exp)).toBe('1.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(versionUpdater(exp, '3.3.3').android.version).toBe('3.3.3');
expect(versionUpdater(exp, '0.0.0').version).toBe('0.0.0');
});
it('should return the correct config target for the ios platform', () => {
const { fieldName, versionGetter, versionUpdater } = getVersionConfigTarget({
exp,
platform: Platform.IOS,
});
expect(fieldName).toBe('expo.ios.version');
expect(versionGetter(exp)).toBe('2.0.0');
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
expect(versionUpdater(exp, '3.3.3').ios.version).toBe('3.3.3');
expect(versionUpdater(exp, '0.0.0').version).toBe('0.0.0');
});
it('should return the correct config target for common version', () => {
const exp: ExpoConfig = {
name: 'test',
slug: 'test',
version: '0.0.0',
};
const { fieldName, versionGetter, versionUpdater } = getVersionConfigTarget({
exp,
platform: Platform.IOS,
});
expect(fieldName).toBe('expo.version');
expect(versionGetter(exp)).toBe('0.0.0');
expect(versionUpdater(exp, '3.3.3').version).toBe('3.3.3');
});
});
+9
View File
@@ -63,3 +63,12 @@ export function getWorkflowRunUrl(
getExpoWebsiteBaseUrl()
).toString();
}
export function getProjectGitHubSettingsUrl(accountName: string, projectName: string): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/github`,
getExpoWebsiteBaseUrl()
).toString();
}
+58 -3
View File
@@ -1,4 +1,5 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
@@ -18,21 +19,25 @@ export async function bumpAppVersionAsync({
appVersion,
projectDir,
exp,
platform,
}: {
appVersion: string;
projectDir: string;
exp: ExpoConfig;
platform: Platform;
}): Promise<void> {
const { fieldName, versionUpdater } = getVersionConfigTarget({ exp, platform });
let bumpedAppVersion: string;
if (semver.valid(appVersion)) {
bumpedAppVersion = nullthrows(semver.inc(appVersion, 'patch'));
Log.log(
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(appVersion)} to ${chalk.bold(
`Bumping ${chalk.bold(fieldName)} from ${chalk.bold(appVersion)} to ${chalk.bold(
bumpedAppVersion
)}`
);
} else {
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(appVersion)} is not a valid semver`);
Log.log(`${chalk.bold(fieldName)} = ${chalk.bold(appVersion)} is not a valid semver`);
bumpedAppVersion = (
await promptAsync({
type: 'text',
@@ -42,6 +47,56 @@ export async function bumpAppVersionAsync({
).bumpedAppVersion;
}
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.version = bumpedAppVersion;
versionUpdater(config, bumpedAppVersion);
});
}
/**
* Get the target version field from ExpoConfig based on the platform.
*/
export function getVersionConfigTarget({
exp,
platform,
}: {
exp: ExpoConfig;
platform: Platform;
}): {
fieldName: string;
versionGetter: (config: ExpoConfig) => string | undefined;
versionUpdater: (config: ExpoConfig, version: string) => ExpoConfig;
} {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
if (platform === Platform.ANDROID && typeof exp.android?.version === 'string') {
return {
fieldName: 'expo.android.version',
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
versionGetter: config => config.android?.version,
versionUpdater: (config, version) => {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
config.android = { ...config.android, version };
return config;
},
};
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
} else if (platform === Platform.IOS && typeof exp.ios?.version === 'string') {
return {
fieldName: 'expo.ios.version',
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
versionGetter: config => config.ios?.version,
versionUpdater: (config, version) => {
// @ts-expect-error: Resolve type errors after upgrading `@expo/config`
config.ios = { ...config.ios, version };
return config;
},
};
}
return {
fieldName: 'expo.version',
versionGetter: config => config.version,
versionUpdater: (config, version) => {
config.version = version;
return config;
},
};
}
+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.`);
@@ -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,22 +42,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables 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 servers: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No readable environment variables found for the "${environment.toLowerCase()}" environment on EAS servers.`
);
}
const encryptedEnvVars = environmentVariables.filter(({ name, value }) => name && !value);
if (encryptedEnvVars.length > 0) {
Log.warn(
`Some environment variables defined in the "${environment.toLowerCase()}" environment on EAS servers are of "encrypted" type and cannot be read outside of the EAS servers (including EAS CLI): ${encryptedEnvVars
.map(({ name }) => name)
.join(', ')}. `
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS servers.`
);
}
Log.newLine();
+12 -12
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',
}),
};
@@ -79,14 +80,13 @@ export const EasJsonOnlyFlag = {
}),
};
export const WithEasEnvironmentVariablesSetFlag = {
'with-eas-environment-variables-set': Flags.enum<EnvironmentVariableEnvironment | null>({
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),
}));
}
@@ -5,6 +5,7 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { evaluateConfigWithEnvVarsAsync } from '../../../build/evaluateConfigWithEnvVarsAsync';
import { getVersionConfigTarget } from '../../../build/utils/version';
import EasCommand from '../../../commandUtils/EasCommand';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
@@ -111,6 +112,7 @@ export default class BuildVersionSetView extends EasCommand {
: `What version would you like to initialize it with?`;
Log.log(currentStateMessage);
const { versionGetter } = getVersionConfigTarget({ exp, platform });
const { version } = await promptAsync({
type: platform === Platform.ANDROID ? 'number' : 'text',
name: 'version',
@@ -124,7 +126,7 @@ export default class BuildVersionSetView extends EasCommand {
appId: projectId,
platform: toAppPlatform(platform),
applicationIdentifier,
storeVersion: exp.version ?? '1.0.0',
storeVersion: versionGetter(exp) ?? '1.0.0',
buildVersion: String(version),
runtimeVersion:
(await getRuntimeVersionNullableAsync(projectDir, exp, platform)) ?? undefined,
@@ -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 };
}
}
+12 -23
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,
@@ -160,29 +158,20 @@ 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.');
}
Log.log(
`Loaded environment variables for the selected environment "${environment.toLowerCase()}":`
);
for (const { name } of nonSecretEnvironmentVariables) {
Log.log(`- ${name}`);
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 servers.`
);
}
Log.newLine();
+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,
});
@@ -3,10 +3,7 @@ import chalk from 'chalk';
import { easJsonExistsAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASNonInteractiveFlag,
WithEasEnvironmentVariablesSetFlag,
} from '../../commandUtils/flags';
import { EASNonInteractiveFlag, EasUpdateEnvironmentFlag } from '../../commandUtils/flags';
import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import {
@@ -24,7 +21,7 @@ export default class UpdateConfigure extends EasCommand {
options: Object.values(RequestedPlatform),
default: RequestedPlatform.All,
}),
...WithEasEnvironmentVariablesSetFlag,
...EasUpdateEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -41,7 +38,7 @@ export default class UpdateConfigure extends EasCommand {
vcsClient,
} = await this.getContextAsync(UpdateConfigure, {
nonInteractive: flags['non-interactive'],
withServerSideEnvironment: flags['with-eas-environment-variables-set'],
withServerSideEnvironment: flags['environment'],
});
Log.log(
+55 -31
View File
@@ -8,14 +8,12 @@ import { transformFingerprintSource } from '../../build/graphql';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import {
EasNonInteractiveAndJsonFlags,
WithEasEnvironmentVariablesSetFlag,
} from '../../commandUtils/flags';
import { EasNonInteractiveAndJsonFlags, EasUpdateEnvironmentFlag } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
EnvironmentVariableEnvironment,
FingerprintInfoGroup,
PublishUpdateGroupInput,
StatuspageServiceName,
UpdateInfoGroup,
@@ -42,6 +40,7 @@ import {
getRuntimeVersionInfoObjectsAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync,
platformDisplayNames,
resolveInputDirectoryAsync,
uploadAssetsAsync,
@@ -75,7 +74,7 @@ type RawUpdateFlags = {
'rollout-percentage'?: number;
'non-interactive': boolean;
json: boolean;
'with-eas-environment-variables-set': EnvironmentVariableEnvironment | null;
environment: EnvironmentVariableEnvironment | null;
};
type UpdateFlags = {
@@ -92,7 +91,7 @@ type UpdateFlags = {
rolloutPercentage?: number;
json: boolean;
nonInteractive: boolean;
withEasEnvironmentVariablesSet: EnvironmentVariableEnvironment | null;
environment: EnvironmentVariableEnvironment | null;
};
export default class UpdatePublish extends EasCommand {
@@ -150,7 +149,7 @@ export default class UpdatePublish extends EasCommand {
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,
}),
...WithEasEnvironmentVariablesSetFlag,
...EasUpdateEnvironmentFlag,
...EasNonInteractiveAndJsonFlags,
};
@@ -178,7 +177,7 @@ export default class UpdatePublish extends EasCommand {
branchName: branchNameArg,
emitMetadata,
rolloutPercentage,
withEasEnvironmentVariablesSet,
environment,
} = this.sanitizeFlags(rawFlags);
const {
@@ -189,7 +188,7 @@ export default class UpdatePublish extends EasCommand {
getServerSideEnvironmentVariablesAsync,
} = await this.getContextAsync(UpdatePublish, {
nonInteractive,
withServerSideEnvironment: withEasEnvironmentVariablesSet,
withServerSideEnvironment: environment,
});
if (jsonFlag) {
@@ -249,7 +248,7 @@ export default class UpdatePublish extends EasCommand {
platformFlag: requestedPlatform,
clearCache,
extraEnv: {
...(withEasEnvironmentVariablesSet
...(environment
? { ...(await getServerSideEnvironmentVariablesAsync()), EXPO_NO_DOTENV: '1' }
: {}),
},
@@ -401,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
@@ -439,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,
@@ -458,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,
};
}
);
@@ -655,14 +679,14 @@ export default class UpdatePublish extends EasCommand {
updateMessage,
inputDir: flags['input-dir'],
skipBundler,
clearCache: flags['clear-cache'] ? true : !!flags['with-eas-environment-variables-set'],
clearCache: flags['clear-cache'] ? true : !!flags['environment'],
platform: flags.platform,
privateKeyPath: flags['private-key-path'],
rolloutPercentage: flags['rollout-percentage'],
nonInteractive,
emitMetadata,
json: flags.json ?? false,
withEasEnvironmentVariablesSet: flags['with-eas-environment-variables-set'],
environment: flags['environment'],
};
}
}
@@ -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,
+20 -5
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 {
@@ -90,10 +92,11 @@ export default class WorkerDeploy extends EasCommand {
helpValue: 'dir',
default: 'dist',
}),
environment: {
...EASEnvironmentFlag.environment,
description: 'Deploy with EAS Environment Variables matching the specified environment.',
},
'dry-run': Flags.boolean({
description: 'Outputs a tarball of the new deployment instead of uploading it.',
default: false,
}),
...EASEnvironmentFlag,
...EasNonInteractiveAndJsonFlags,
};
@@ -268,6 +271,16 @@ export default class WorkerDeploy extends EasCommand {
})
);
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 +395,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'],
};
}
}
@@ -0,0 +1,121 @@
import chalk from 'chalk';
import fs from 'fs/promises';
import fsExtra from 'fs-extra';
import path from 'path';
import prompts from 'prompts';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import Log from '../../log';
import { WorkflowFile } from '../../utils/workflowFile';
const DEFAULT_WORKFLOW_NAME = 'workflow.yml';
const HELLO_WORLD_TEMPLATE = `name: Hello World
on:
push:
branches: ['*']
jobs:
hello_world:
steps:
- uses: eas/checkout
- run: echo "Hello, World"
`;
export class WorkflowCreate extends EasCommand {
static override description = 'create a new workflow configuration YAML file';
static override args = [
{
name: 'name',
description: 'Name of the workflow file (must end with .yml or .yaml)',
required: false,
},
];
static override flags = {
...EASNonInteractiveFlag,
};
static override contextDefinition = {
...this.ContextOptions.ProjectDir,
};
async runAsync(): Promise<void> {
const {
args: { name: argFileName },
flags,
} = await this.parse(WorkflowCreate);
const { projectDir } = await this.getContextAsync(WorkflowCreate, {
nonInteractive: flags['non-interactive'],
});
let fileName = argFileName;
if (!fileName) {
const response = await prompts({
type: 'text',
name: 'fileName',
message: 'What would you like to name your workflow file?',
initial: DEFAULT_WORKFLOW_NAME,
validate: value => {
try {
WorkflowFile.validateYamlExtension(value);
return true;
} catch (error) {
return error instanceof Error ? error.message : 'Invalid file name';
}
},
});
if (!response.fileName) {
Log.warn('Workflow creation cancelled.');
process.exit(0);
}
fileName = response.fileName;
}
try {
await this.ensureWorkflowsDirectoryExistsAsync({ projectDir });
await this.createWorkflowFileAsync({ fileName, projectDir });
} catch (error) {
Log.error('Failed to create workflow file.');
throw error;
}
}
private async ensureWorkflowsDirectoryExistsAsync({
projectDir,
}: {
projectDir: string;
}): Promise<void> {
try {
await fs.access(path.join(projectDir, '.eas', 'workflows'));
} catch {
await fs.mkdir(path.join(projectDir, '.eas', 'workflows'), { recursive: true });
Log.withTick(`Created directory ${chalk.bold(path.join(projectDir, '.eas', 'workflows'))}`);
}
}
private async createWorkflowFileAsync({
fileName,
projectDir,
}: {
fileName: string;
projectDir: string;
}): Promise<void> {
WorkflowFile.validateYamlExtension(fileName);
const filePath = path.join(projectDir, '.eas', 'workflows', fileName);
if (await fsExtra.pathExists(filePath)) {
throw new Error(`Workflow file already exists: ${filePath}`);
}
await fs.writeFile(filePath, HELLO_WORLD_TEMPLATE);
Log.withTick(`Created ${chalk.bold(filePath)}`);
}
}
+26 -8
View File
@@ -1,23 +1,21 @@
import fs from 'node:fs';
import { CombinedError } from '@urql/core';
import * as path from 'node:path';
import { getWorkflowRunUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WorkflowProjectSourceType } from '../../graphql/generated';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
import Log, { link } from '../../log';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { uploadAccountScopedEasJsonAsync } from '../../project/uploadAccountScopedEasJsonAsync';
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
import { WorkflowFile } from '../../utils/workflowFile';
export default class WorkflowRun extends EasCommand {
static override description = 'Run an EAS workflow';
// TODO(@sjchmiela): Keep command hidden until workflows are live
static override hidden = true;
static override state = 'beta';
static override args = [{ name: 'file', description: 'Path to the workflow file to run' }];
static override flags = {
@@ -34,8 +32,6 @@ export default class WorkflowRun extends EasCommand {
async runAsync(): Promise<void> {
const { flags, args } = await this.parse(WorkflowRun);
Log.warn('Workflows are in beta and subject to breaking changes.');
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -48,7 +44,12 @@ export default class WorkflowRun extends EasCommand {
let yamlConfig: string;
try {
yamlConfig = await fs.promises.readFile(path.join(projectDir, args.file), 'utf8');
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
projectDir,
filePath: args.file,
});
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
yamlConfig = workflowFileContents.yamlConfig;
} catch (err) {
Log.error('Failed to read workflow file.');
@@ -61,6 +62,23 @@ export default class WorkflowRun extends EasCommand {
} = await getDynamicPrivateProjectConfigAsync();
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
try {
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
appId: projectId,
yamlConfig,
});
} catch (error) {
if (error instanceof CombinedError) {
WorkflowFile.maybePrintWorkflowFileValidationErrors({
error,
accountName: account.name,
projectName,
});
throw error;
}
}
let projectArchiveBucketKey: string;
let easJsonBucketKey: string;
@@ -1,14 +1,16 @@
import fs from 'fs/promises';
import path from 'path';
import { CombinedError } from '@urql/core';
import * as YAML from 'yaml';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
import Log from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { WorkflowFile } from '../../utils/workflowFile';
export class WorkflowValidate extends EasCommand {
static override description = 'validate a workflow configuration yaml file';
static override hidden = true;
static override args = [
{
@@ -18,81 +20,83 @@ export class WorkflowValidate extends EasCommand {
},
];
static override flags = {
...EASNonInteractiveFlag,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const {
args: { path: filePath },
flags,
} = await this.parse(WorkflowValidate);
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
projectDir,
} = await this.getContextAsync(WorkflowValidate, {
nonInteractive: flags['non-interactive'],
withServerSideEnvironment: null,
});
const {
projectId,
exp: { slug: projectName },
} = await getDynamicPrivateProjectConfigAsync();
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const spinner = ora().start('Validating the workflow YAML file…');
try {
await validateYAMLAsync(filePath);
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
projectDir,
filePath,
});
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
// Check if the parsed result is empty or null
if (
parsedYaml === null ||
parsedYaml === undefined ||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
) {
throw new Error('YAML file is empty or contains only comments.');
}
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
appId: projectId,
yamlConfig: workflowFileContents.yamlConfig,
});
spinner.succeed('Workflow configuration YAML is valid.');
} catch (error) {
spinner.fail('Workflow configuration YAML is not valid.');
if (error instanceof YAML.YAMLParseError) {
Log.error(`YAML syntax error: ${error.message}`);
} else if (error instanceof CombinedError) {
WorkflowFile.maybePrintWorkflowFileValidationErrors({
error,
accountName: account.name,
projectName,
});
throw error;
} else if (error instanceof Error) {
Log.error(error.message);
} else {
Log.error(`Unexpected error: ${String(error)}`);
}
process.exit(1);
}
}
}
async function checkIfFileExistsAsync(filePath: string): Promise<void> {
try {
await fs.access(filePath);
} catch {
throw new Error(`File does not exist: ${filePath}`);
}
}
function checkIfValidYAMLFileExtension(filePath: string): void {
const fileExtension = path.extname(filePath).toLowerCase();
if (fileExtension !== '.yml' && fileExtension !== '.yaml') {
throw new Error('File must have a .yml or .yaml extension');
}
}
async function validateYAMLAsync(filePath: string): Promise<void> {
await checkIfFileExistsAsync(filePath);
checkIfValidYAMLFileExtension(filePath);
try {
const fileContents = await fs.readFile(filePath, 'utf8');
// First check if the file is empty or only contains whitespace
if (!fileContents.trim()) {
throw new Error('YAML file is empty or contains only comments.');
}
const parsedYaml = YAML.parse(fileContents);
// Check if the parsed result is empty or null
if (
parsedYaml === null ||
parsedYaml === undefined ||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
) {
throw new Error('YAML file is empty or contains only comments.');
}
} catch (error) {
if (
error instanceof Error &&
error.message === 'YAML file is empty or contains only comments.'
) {
throw error;
}
if (error instanceof YAML.YAMLParseError || error instanceof Error) {
throw new Error(`YAML parsing error: ${error.message}`);
} else {
throw new Error(`YAML parsing error: ${String(error)}`);
}
}
}
@@ -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 =>
+128 -50
View File
@@ -711,6 +711,7 @@ export type AccountSsoConfigurationPublicDataQueryPublicDataByAccountNameArgs =
};
export enum AccountUploadSessionType {
ProfileImageUpload = 'PROFILE_IMAGE_UPLOAD',
WorkflowsProjectSources = 'WORKFLOWS_PROJECT_SOURCES'
}
@@ -767,7 +768,8 @@ export enum ActivityTimelineProjectActivityType {
Build = 'BUILD',
Submission = 'SUBMISSION',
Update = 'UPDATE',
Worker = 'WORKER'
Worker = 'WORKER',
WorkflowRun = 'WORKFLOW_RUN'
}
/** A regular user, SSO user, or robot that can authenticate with Expo services and be a member of accounts. */
@@ -1309,8 +1311,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
@@ -1745,7 +1748,6 @@ export type AppInfoInput = {
export type AppInput = {
accountId: Scalars['ID']['input'];
appInfo?: InputMaybe<AppInfoInput>;
privacy: AppPrivacy;
projectName: Scalars['String']['input'];
};
@@ -2047,6 +2049,10 @@ export type AppUpdatesConnection = {
pageInfo: PageInfo;
};
export enum AppUploadSessionType {
ProfileImageUpload = 'PROFILE_IMAGE_UPLOAD'
}
/** Represents Play Store/App Store version of an application */
export type AppVersion = {
__typename?: 'AppVersion';
@@ -2097,7 +2103,6 @@ export type AppWithGithubRepositoryInput = {
accountId: Scalars['ID']['input'];
appInfo?: InputMaybe<AppInfoInput>;
installationIdentifier?: InputMaybe<Scalars['String']['input']>;
privacy: AppPrivacy;
projectName: Scalars['String']['input'];
};
@@ -2689,6 +2694,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']>;
@@ -2722,6 +2728,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
@@ -2864,6 +2871,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']>;
@@ -2875,6 +2883,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']>;
@@ -3388,12 +3397,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']>;
@@ -3629,11 +3632,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';
@@ -4179,6 +4177,35 @@ export enum Feature {
Teams = 'TEAMS'
}
export type Fingerprint = {
__typename?: 'Fingerprint';
app: App;
createdAt: Scalars['DateTime']['output'];
debugInfoUrl?: Maybe<Scalars['String']['output']>;
hash: Scalars['String']['output'];
id: Scalars['ID']['output'];
source?: Maybe<FingerprintSource>;
updatedAt: Scalars['DateTime']['output'];
};
export type FingerprintInfo = {
fingerprintHash: Scalars['String']['input'];
fingerprintSource: FingerprintSourceInput;
};
export type FingerprintInfoGroup = {
android?: InputMaybe<FingerprintInfo>;
ios?: InputMaybe<FingerprintInfo>;
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']>;
@@ -4975,6 +5002,7 @@ export type JobRun = {
createdAt: Scalars['DateTime']['output'];
displayName?: Maybe<Scalars['String']['output']>;
endedAt?: Maybe<Scalars['DateTime']['output']>;
errors: Array<JobRunError>;
expiresAt: Scalars['DateTime']['output'];
gitCommitHash?: Maybe<Scalars['String']['output']>;
gitCommitMessage?: Maybe<Scalars['String']['output']>;
@@ -4990,6 +5018,14 @@ export type JobRun = {
updateGroups: Array<Array<Update>>;
};
export type JobRunError = {
__typename?: 'JobRunError';
buildPhase?: Maybe<Scalars['String']['output']>;
docsUrl?: Maybe<Scalars['String']['output']>;
errorCode: Scalars['String']['output'];
message: Scalars['String']['output'];
};
export type JobRunMutation = {
__typename?: 'JobRunMutation';
/** Cancel an EAS Job Run */
@@ -5477,6 +5513,7 @@ export type PublishUpdateGroupInput = {
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']['input']>;
branchId: Scalars['String']['input'];
excludedAssets?: InputMaybe<Array<PartialManifestAsset>>;
fingerprintInfoGroup?: InputMaybe<FingerprintInfoGroup>;
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
isGitWorkingTreeDirty?: InputMaybe<Scalars['Boolean']['input']>;
message?: InputMaybe<Scalars['String']['input']>;
@@ -5722,7 +5759,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;
@@ -5739,6 +5775,7 @@ export type RootMutation = {
/** Mutations that modify a websiteNotification */
websiteNotifications: WebsiteNotificationMutation;
workflowJob: WorkflowJobMutation;
workflowRevision: WorkflowRevisionMutation;
workflowRun: WorkflowRunMutation;
};
@@ -6124,28 +6161,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 */
@@ -6535,6 +6550,7 @@ export type Update = ActivityTimelineProjectActivity & {
createdAt: Scalars['DateTime']['output'];
deployments: DeploymentResult;
expoGoSDKVersion?: Maybe<Scalars['String']['output']>;
fingerprint?: Maybe<Fingerprint>;
gitCommitHash?: Maybe<Scalars['String']['output']>;
group: Scalars['String']['output'];
id: Scalars['ID']['output'];
@@ -6848,6 +6864,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'];
};
@@ -6859,6 +6877,12 @@ export type UploadSessionCreateAccountScopedUploadSessionArgs = {
};
export type UploadSessionCreateAppScopedUploadSessionArgs = {
appID: Scalars['ID']['input'];
type: AppUploadSessionType;
};
export type UploadSessionCreateUploadSessionArgs = {
type: UploadSessionType;
};
@@ -7627,6 +7651,7 @@ export type WorkerDeployment = ActivityTimelineProjectActivity & {
logs?: Maybe<WorkerDeploymentLogs>;
requests?: Maybe<WorkerDeploymentRequests>;
subdomain: Scalars['String']['output'];
tier?: Maybe<WorkerDeploymentTier>;
url: Scalars['String']['output'];
};
@@ -7979,6 +8004,11 @@ export type WorkerDeploymentRequestsTimeseriesEdge = {
timestamp: Scalars['DateTime']['output'];
};
export enum WorkerDeploymentTier {
Free = 'FREE',
Paid = 'PAID'
}
export type WorkerDeploymentsConnection = {
__typename?: 'WorkerDeploymentsConnection';
edges: Array<WorkerDeploymentEdge>;
@@ -8073,6 +8103,7 @@ export enum WorkflowJobStatus {
Failure = 'FAILURE',
InProgress = 'IN_PROGRESS',
New = 'NEW',
PendingCancel = 'PENDING_CANCEL',
Skipped = 'SKIPPED',
Success = 'SUCCESS'
}
@@ -8081,6 +8112,7 @@ export enum WorkflowJobType {
AppleDeviceRegistrationRequest = 'APPLE_DEVICE_REGISTRATION_REQUEST',
Build = 'BUILD',
Custom = 'CUSTOM',
GetBuild = 'GET_BUILD',
MaestroTest = 'MAESTRO_TEST',
RequireApproval = 'REQUIRE_APPROVAL',
Submission = 'SUBMISSION',
@@ -8130,6 +8162,17 @@ export type WorkflowRevisionInput = {
yamlConfig: Scalars['String']['input'];
};
export type WorkflowRevisionMutation = {
__typename?: 'WorkflowRevisionMutation';
validateWorkflowYamlConfig: Scalars['Boolean']['output'];
};
export type WorkflowRevisionMutationValidateWorkflowYamlConfigArgs = {
appId: Scalars['ID']['input'];
yamlConfig: Scalars['String']['input'];
};
export type WorkflowRevisionQuery = {
__typename?: 'WorkflowRevisionQuery';
byId: WorkflowRevision;
@@ -8146,9 +8189,12 @@ export type WorkflowRevisionsConnection = {
pageInfo: PageInfo;
};
export type WorkflowRun = {
export type WorkflowRun = ActivityTimelineProjectActivity & {
__typename?: 'WorkflowRun';
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>;
@@ -8170,16 +8216,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;
@@ -8202,6 +8260,7 @@ export enum WorkflowRunStatus {
Failure = 'FAILURE',
InProgress = 'IN_PROGRESS',
New = 'NEW',
PendingCancel = 'PENDING_CANCEL',
Success = 'SUCCESS'
}
@@ -8884,7 +8943,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'];
@@ -8900,7 +8959,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'];
@@ -8967,6 +9026,14 @@ export type DeleteWebhookMutationVariables = Exact<{
export type DeleteWebhookMutation = { __typename?: 'RootMutation', webhook: { __typename?: 'WebhookMutation', deleteWebhook: { __typename?: 'DeleteWebhookResult', id: string } } };
export type ValidateWorkflowYamlConfigMutationVariables = Exact<{
appId: Scalars['ID']['input'];
yamlConfig: Scalars['String']['input'];
}>;
export type ValidateWorkflowYamlConfigMutation = { __typename?: 'RootMutation', workflowRevision: { __typename?: 'WorkflowRevisionMutation', validateWorkflowYamlConfig: boolean } };
export type CreateWorkflowRunMutationVariables = Exact<{
appId: Scalars['ID']['input'];
workflowRevisionInput: WorkflowRevisionInput;
@@ -9031,7 +9098,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'];
@@ -9052,7 +9119,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'];
@@ -9068,6 +9135,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'];
@@ -9085,7 +9159,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'];
@@ -9094,7 +9168,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'];
@@ -9218,7 +9292,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'];
@@ -9229,7 +9303,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'];
@@ -9239,7 +9313,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; }>;
@@ -9269,21 +9343,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;
};
@@ -0,0 +1,48 @@
import gql from 'graphql-tag';
import { z } from 'zod';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
ValidateWorkflowYamlConfigMutation,
ValidateWorkflowYamlConfigMutationVariables,
} from '../generated';
export namespace WorkflowRevisionMutation {
export const ValidationErrorExtensionZ = z.object({
errorCode: z.literal('VALIDATION_ERROR'),
metadata: z.object({
formErrors: z.array(z.string()),
fieldErrors: z.record(z.string(), z.array(z.string())),
}),
});
export async function validateWorkflowYamlConfigAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
yamlConfig,
}: {
appId: string;
yamlConfig: string;
}
): Promise<void> {
await withErrorHandlingAsync(
graphqlClient
.mutation<ValidateWorkflowYamlConfigMutation, ValidateWorkflowYamlConfigMutationVariables>(
gql`
mutation ValidateWorkflowYamlConfig($appId: ID!, $yamlConfig: String!) {
workflowRevision {
validateWorkflowYamlConfig(appId: $appId, yamlConfig: $yamlConfig)
}
}
`,
{
appId,
yamlConfig,
}
)
.toPromise()
);
}
}
@@ -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;
@@ -90,7 +89,6 @@ export async function fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsyn
const id = await AppMutation.createAppAsync(graphqlClient, {
accountId: account.id,
projectName,
privacy: projectInfo.privacy ?? AppPrivacy.Public,
});
spinner.succeed(`Created ${chalk.bold(projectLink)} on Expo`);
return id;
+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,
};
}
@@ -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'));
}
+3 -3
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}`);
@@ -67,7 +67,7 @@ export async function promptVariableVisibilityAsync(
selected: selectedVisibility === EnvironmentVariableVisibility.Sensitive,
},
{
title: 'Encrypted',
title: 'Secret',
value: EnvironmentVariableVisibility.Secret,
selected: selectedVisibility === EnvironmentVariableVisibility.Secret,
},
@@ -0,0 +1,85 @@
import { CombinedError } from '@urql/core';
import fs from 'fs';
import path from 'path';
import { getProjectGitHubSettingsUrl } from '../build/utils/url';
import { WorkflowRevisionMutation } from '../graphql/mutations/WorkflowRevisionMutation';
import Log, { link } from '../log';
export namespace WorkflowFile {
export async function readWorkflowFileContentsAsync({
projectDir,
filePath,
}: {
projectDir: string;
filePath: string;
}): Promise<{ yamlConfig: string; filePath: string }> {
const [yamlFromEasWorkflowsFile, yamlFromFile] = await Promise.allSettled([
fs.promises.readFile(path.join(projectDir, '.eas', 'workflows', filePath), 'utf8'),
fs.promises.readFile(path.join(process.cwd(), filePath), 'utf8'),
]);
// We prioritize .eas/workflows/${file} over ${file}, because
// in the worst case we'll try to read .eas/workflows/.eas/workflows/test.yml,
// which is likely not to exist.
if (yamlFromEasWorkflowsFile.status === 'fulfilled') {
return {
yamlConfig: yamlFromEasWorkflowsFile.value,
filePath: path.join(projectDir, '.eas', 'workflows', filePath),
};
} else if (yamlFromFile.status === 'fulfilled') {
return {
yamlConfig: yamlFromFile.value,
filePath: path.join(process.cwd(), filePath),
};
}
throw yamlFromFile.reason;
}
export function maybePrintWorkflowFileValidationErrors({
error,
accountName,
projectName,
}: {
error: CombinedError;
accountName: string;
projectName: string;
}): void {
const validationErrors = error.graphQLErrors.flatMap(e => {
return WorkflowRevisionMutation.ValidationErrorExtensionZ.safeParse(e.extensions).data ?? [];
});
if (validationErrors.length > 0) {
Log.error('Workflow file is invalid. Issues:');
for (const validationError of validationErrors) {
for (const formError of validationError.metadata.formErrors) {
Log.error(`- ${formError}`);
}
for (const [field, fieldErrors] of Object.entries(validationError.metadata.fieldErrors)) {
Log.error(`- ${field}: ${fieldErrors.join(', ')}`);
}
}
}
const githubNotFoundError = error.graphQLErrors.find(
e => e.extensions.errorCode === 'GITHUB_NOT_FOUND_ERROR'
);
if (githubNotFoundError) {
Log.error(`GitHub repository not found. It is currently required to run workflows.`);
Log.error(
`Please check that the repository exists and that you have access to it. ${link(
getProjectGitHubSettingsUrl(accountName, projectName)
)}`
);
}
}
export function validateYamlExtension(fileName: string): void {
const fileExtension = path.extname(fileName).toLowerCase();
if (fileExtension !== '.yml' && fileExtension !== '.yaml') {
throw new Error('File must have a .yml or .yaml extension');
}
}
}
+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
+1 -1
View File
@@ -18,6 +18,6 @@ export default class NoVcsClient extends Client {
}
public override canGetLastCommitMessage(): boolean {
return true;
return false;
}
}
+5 -4
View File
@@ -109,20 +109,21 @@ export async function uploadAsync(params: UploadParams): Promise<UploadResult> {
return retry(error);
}
const body = await response.json().catch(() => null);
const errorMessage = 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(errorMessage));
} 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(errorMessage);
}
return {
@@ -1,109 +0,0 @@
import fs from 'fs/promises';
import { vol } from 'memfs';
import { WorkflowValidate } from '../../commands/workflow/validate';
jest.mock('fs/promises');
jest.mock('../../log');
jest.mock('../../ora', () => ({
ora: () => ({
start: () => ({
succeed: jest.fn(),
fail: jest.fn(),
}),
}),
}));
describe('WorkflowValidate', () => {
const mockValidYaml = `
name: workflow
on: push
jobs:
build:
steps:
- run: echo "Hello"
`;
const mockInvalidYaml = `
name: workflow
on: : : : invalid
`;
let command: WorkflowValidate;
beforeEach(() => {
vol.reset();
jest.resetAllMocks();
process.exit = jest.fn(() => {
throw new Error();
});
});
describe('file validation', () => {
describe('when file exists', () => {
beforeEach(() => {
(fs.access as jest.Mock).mockResolvedValue(undefined);
});
it('validates a correct YAML file', async () => {
(fs.readFile as jest.Mock).mockResolvedValue(mockValidYaml);
command = new WorkflowValidate(['./workflow.yml'], {} as any);
await expect(command.runAsync()).resolves.not.toThrow();
});
it('accepts both .yml and .yaml extensions', async () => {
(fs.readFile as jest.Mock).mockResolvedValue(mockValidYaml);
command = new WorkflowValidate(['./workflow.yml'], {} as any);
await expect(command.runAsync()).resolves.not.toThrow();
command = new WorkflowValidate(['./workflow.yaml'], {} as any);
await expect(command.runAsync()).resolves.not.toThrow();
});
it('throws error for empty YAML file', async () => {
(fs.readFile as jest.Mock).mockResolvedValue('');
command = new WorkflowValidate(['./workflow.yml'], {} as any);
await expect(command.runAsync()).rejects.toThrow();
});
it('throws error for invalid YAML syntax', async () => {
(fs.readFile as jest.Mock).mockResolvedValue(mockInvalidYaml);
command = new WorkflowValidate(['./workflow.yml'], {} as any);
await expect(command.runAsync()).rejects.toThrow();
});
it('throws error for invalid file extension', async () => {
command = new WorkflowValidate(['./workflow.txt'], {} as any);
await expect(command.runAsync()).rejects.toThrow();
});
});
describe('when file does not exist', () => {
beforeEach(() => {
(fs.access as jest.Mock).mockRejectedValue(new Error('ENOENT'));
});
it('throws appropriate error', async () => {
command = new WorkflowValidate(['./nonexistent.yml'], {} as any);
await expect(command.runAsync()).rejects.toThrow();
});
});
});
describe('error handling', () => {
it('handles unexpected errors gracefully', async () => {
(fs.access as jest.Mock).mockResolvedValue(undefined);
(fs.readFile as jest.Mock).mockRejectedValue(new Error('Unexpected error'));
command = new WorkflowValidate(['./workflow.yml'], {} as any);
await expect(command.runAsync()).rejects.toThrow();
});
});
});
+1 -1
View File
@@ -10,7 +10,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"target": "ES2021",
"target": "ES2022",
"outDir": "build",
"rootDir": "src",
"typeRoots": ["../../ts-declarations", "../../node_modules/@types", "./node_modules/@types"]
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more