Compare commits

...
Author SHA1 Message Date
Will Schurman bf27ec5238 v16.14.0 2025-07-07 15:47:08 -07:00
Will Schurman 🦦 8f18b35fe8 [eas-cli] Fix graphql codegen to look in all directories (#3097)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

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

# How

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

# Test Plan

```
neas logout
neas login
neas whoami
```
2025-07-07 14:24:25 -07:00
Will Schurman 🦦 e82b8966b0 [eas-cli] Add update:revert-update-rollout command (#3068)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

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

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

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

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

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

# How

Add command, refactoring shared code where necessary.

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

So, 8 total cases.

# Test Plan

Tested most of the 8 cases.

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

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

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

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

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

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

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

Empty branch rollout (both platforms), no control:

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

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

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

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

# How

Add a new workflow that

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

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

# Test Plan

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

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

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

Test Plan
===
Just automated tests since this is a copy change.
2025-06-30 15:55:32 -07:00
Rolf Bjaanes 80dfcdbe29 [eas-cli] Update systemRequirements.ts with correct fix for missing Simulator (#3079)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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


# How

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


# Test Plan

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

* Temporary Commit at 6/27/2025, 4:17:10 PM
2025-06-27 16:21:15 -07:00
Expo CI 683a778b15 update CHANGELOG.md 2025-06-25 20:35:04 +00:00
Stanisław Chmiela 43ccaa6129 v16.13.1 2025-06-25 22:29:32 +02:00
Stanisław Chmiela d2c4608f3f [eas-cli] Add groups output to submit:internal (#3075)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

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

# How

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

# Test Plan

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

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

<img width="253" alt="Zrzut ekranu 2025-06-25 o 17 39 22" src="https://github.com/user-attachments/assets/584e1bea-a905-4ba3-a68e-5df4a4d3477e" />
2025-06-25 22:10:10 +02:00
Will Schurman 🦦 d6b2caa4d6 [eas-cli] Fix update group deletion (#3069)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

# How

Disable cache for the poll request.

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

# Test Plan

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

See it now works.
2025-06-24 11:33:43 -07:00
Will Schurman 🦦 78cd4bf86a [eas-cli] Fix bug in log during update:republish (#3067)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

# How

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

# Test Plan

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

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

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

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

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

* Apply suggestions from code review

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

---------

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

* Apply suggestions from code review

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

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
2025-06-20 12:15:38 -07:00
Expo CI ec38cdb154 update CHANGELOG.md 2025-06-20 12:39:17 +00:00
Stanisław Chmiela 2b89aed724 v16.12.0 2025-06-20 14:15:13 +02:00
Stanisław Chmiela b9334ab7a3 [eas-cli] Add projectRootDirectory to workflow:run payload (#3058)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

# How

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

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

# Test Plan

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

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

# How

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

# Test Plan

I have run following command (easd linked to locally build eas-cli, as described in CONTRIBUTING.md)
```
EXPO_DEBUG=1 easd build:dev -e preview -p android
```
2025-06-13 14:21:07 +02:00
Will Schurman 430181893f [eas-cli] Fix changelog merge conflict (#3052)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

# How

Move the entry to the correct release.

# Test Plan

Inspect.
2025-06-12 09:44:55 -07:00
Will Schurman 6636f76cff [eas-cli] Allow rolling out update on empty branch (#3050)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

# How

Remove error when no existing update exists.

# Test Plan

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

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

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

# How

remove repack relevant code

# Test Plan

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

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

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

# How

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

# Test Plan

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

# Deployment plan

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

Co-authored-by: Stanisław Chmiela <[email protected]>
2025-06-06 17:33:37 +02:00
Douglas Lowder dda4fe45be [eas-cli] Fix workflow:runs query sort order (#3045) 2025-06-04 13:10:56 -07:00
Expo CI d5c0b1b3d7 update CHANGELOG.md 2025-06-04 11:33:53 +00:00
Stanisław Chmiela f968654ffc v16.8.0 2025-06-04 13:27:33 +02:00
Stanisław Chmiela a66753cde9 [eas-cli] Allow skipping version check (#3041)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C089ED313PV/p1748545207578949?thread_ts=1748543276.564549&cid=C089ED313PV

# How

Added support for `EAS_SKIP_CLI_VERSION_CHECK` environment variable.

# Test Plan

Added tests.
2025-06-04 13:26:57 +02:00
Douglas LowderandWill Schurman 0c85c68948 [eas-cli] workflow:list and workflow:runs commands (#3030)
Co-authored-by: Will Schurman <[email protected]>
2025-06-03 19:41:31 -07:00
Will Schurman 78770912ab [eas-cli] Change location of EAS Update host override config (#3042)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

Closes ENG-15925.

# How

Change location.

# Test Plan

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

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

# How

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

# Test Plan

Added tests.
2025-06-03 15:56:54 +02:00
Expo CI 25cc555189 update CHANGELOG.md 2025-06-03 07:25:43 +00:00
Stanisław Chmiela be73d3a337 v16.7.1 2025-06-03 09:19:10 +02:00
Stanisław Chmiela ecb6458a28 [eas-cli] Do not require eas.json or package.json to create a workflow run (#3028)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

# How

Check before uploading the non-required files.

# Test Plan

<img width="946" alt="Zrzut ekranu 2025-06-2 o 21 45 31" src="https://github.com/user-attachments/assets/b6b4aad1-608f-471c-b541-302850744062" />
2025-06-03 09:18:17 +02:00
Will Schurman 475d90ea23 [eas-cli] Fix update republishing not including environment or git info (#3036)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

These fields were missing in republished updates.

Closes ENG-15846.

# How

Copy these fields over as well when republishing.

# Test Plan

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

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

* Temporary Commit at 5/29/2025, 12:17:09 PM
2025-05-29 12:21:56 +02:00
Will Schurman 5caef3bc05 [eas-json][eas-cli] Add support for manifest host and asset host overriding (#3021)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

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

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

Closes ENG-15804.

# How

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

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

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

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

# Test Plan

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

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

* Add changelog entry

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

Affirm asked for this.

# How

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

# Test Plan

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


`--json`:
```
{
  "id": "0196c563-5a25-75e1-9447-e35d82f8fef0",
  "url": "http://expo.test/accounts/stanley/projects/test-local/workflows/0196c563-5a25-75e1-9447-e35d82f8fef0"
}
```
2025-05-13 11:00:40 +02:00
Stanisław Chmiela e7f687b45d [eas-cli] Add --wait flag to workflow:run (#3012)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C083Q88U4M8/p1747060067200209

# How

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

# Test Plan

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

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

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

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

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

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

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

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

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

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

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

ARGUMENTS
  FILE  Path to the workflow file to run

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

DESCRIPTION
  Run an EAS workflow

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

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

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

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

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

# How

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

# Test Plan

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

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

* Add changelog entry

* Fix lint
2025-05-02 13:33:28 +02:00
Gabriel Donadel f83c7d8455 Revert "Add changelog entry"
This reverts commit 5aedf13af1.
2025-04-30 16:32:51 -03:00
Gabriel Donadel 5aedf13af1 Add changelog entry 2025-04-30 16:16:46 -03:00
Gabriel Donadel 39030e9a3e Update readme 2025-04-30 16:15:30 -03:00
106 changed files with 5674 additions and 2493 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@ commentConfiguration:
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['khamilowicz', 'sjchmiela']
mentions: ['sjchmiela']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/update/**']
+29
View File
@@ -0,0 +1,29 @@
name: Ensure GraphQL schema and generated code is up-to-date
on:
push:
branches: [main]
pull_request:
branches: [main]
paths:
- .github/workflows/test-graphql.yml
- packages/eas-cli/src/**
schedule:
- cron: '0 8 * * *' # 8:00 AM UTC time every day
concurrency:
group: test-graphql-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 22
- run: yarn install --frozen-lockfile
- run: yarn verify-graphql-code
working-directory: packages/eas-cli
@@ -66,7 +66,7 @@ jobs:
destination_branch: 'main'
pr_title: 'Upgrade eas-build dependencies'
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
pr_reviewer: 'radoslawkrzemien,sjchmiela,khamilowicz'
pr_reviewer: 'sjchmiela'
github_token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/github-script@v3
+166 -3
View File
@@ -8,14 +8,177 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
-- Add `--verbose-logs` flag for `build` command ([#3000](https://github.com/expo/eas-cli/pull/3000) by [@khamilowicz](https://github.com/khamilowicz))
- Add `eas-build-cache-provider` package. ([#3002](https://github.com/expo/eas-cli/pull/3002) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Add update:revert-update-rollout command. ([#3068](https://github.com/expo/eas-cli/pull/3068) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
### 🧹 Chores
- Remove unreleased `link` and `unlink` commands from `eas env`. ([#3092](https://github.com/expo/eas-cli/pull/3092) by [@kadikraman](https://github.com/kadikraman))
- Add new CI to verify GraphQL generation. ([#3093](https://github.com/expo/eas-cli/pull/3093) by [@douglowder](https://github.com/douglowder))
- Update GraphQL generation to look in all src directories. ([#3097](https://github.com/expo/eas-cli/pull/3097) by [@wschurman](https://github.com/wschurman))
## [16.13.4](https://github.com/expo/eas-cli/releases/tag/v16.13.4) - 2025-07-04
### 🧹 Chores
- Remove hidden flag from `eas upload`. ([#3014](https://github.com/expo/eas-cli/pull/3014) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.13.3](https://github.com/expo/eas-cli/releases/tag/v16.13.3) - 2025-07-01
### 🐛 Bug fixes
- Fix suggested command to switch Xcode Developer Tools. ([#3079](https://github.com/expo/eas-cli/pull/3079) by [@rolfb](https://github.com/rolfb))
- Skip configuring build profiles when running `eas update:configure` if no build profiles exist. ([#3084](https://github.com/expo/eas-cli/pull/3084) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Reword how paid plans are described ([#3082](https://github.com/expo/eas-cli/pull/3082) by [@ide](https://github.com/ide))
## [16.13.2](https://github.com/expo/eas-cli/releases/tag/v16.13.2) - 2025-06-27
### 🧹 Chores
- Amend update override fields. ([#3080](https://github.com/expo/eas-cli/pull/3080) by [@quinlanj](https://github.com/quinlanj))
## [16.13.1](https://github.com/expo/eas-cli/releases/tag/v16.13.1) - 2025-06-25
### 🐛 Bug fixes
- Fix bug in log during update:republish. ([#3067](https://github.com/expo/eas-cli/pull/3067) by [@wschurman](https://github.com/wschurman))
- Fix update group deletion. ([#3069](https://github.com/expo/eas-cli/pull/3069) by [@wschurman](https://github.com/wschurman))
## [16.13.0](https://github.com/expo/eas-cli/releases/tag/v16.13.0) - 2025-06-24
### 🎉 New features
- Add `--what-to-test` option to `build` command. ([#3065](https://github.com/expo/eas-cli/pull/3065) by [@vonovak](https://github.com/vonovak))
### 🐛 Bug fixes
- Hide `workflow:list`. ([#3060](https://github.com/expo/eas-cli/pull/3060) by [@douglowder](https://github.com/douglowder))
- Align text of messages from `build` and `workflow:run`. ([#3061](https://github.com/expo/eas-cli/pull/3061) by [@douglowder](https://github.com/douglowder))
## [16.12.0](https://github.com/expo/eas-cli/releases/tag/v16.12.0) - 2025-06-20
### 🎉 New features
- Add `--what-to-test` option to the `submit` command. ([#3023](https://github.com/expo/eas-cli/pull/3023) by [@vonovak](https://github.com/vonovak))
### 🐛 Bug fixes
- Fixed monorepo support in `workflow:run` if a project is not connected to a GitHub repository. ([#3058](https://github.com/expo/eas-cli/pull/3058) by [@sjchmiela](https://github.com/sjchmiela))
## [16.10.1](https://github.com/expo/eas-cli/releases/tag/v16.10.1) - 2025-06-13
### 🐛 Bug fixes
- Fixed `eas build:dev` command to correctly use passed build profile. ([#3053](https://github.com/expo/eas-cli/pull/3053) by [@sebryu](https://github.com/sebryu))
### 🧹 Chores
- Allow rolling out update on empty branch. ([#3050](https://github.com/expo/eas-cli/pull/3050) by [@wschurman](https://github.com/wschurman))
## [16.10.0](https://github.com/expo/eas-cli/releases/tag/v16.10.0) - 2025-06-12
### 🎉 New features
- [eas-cli] Add workflow:cancel. ([#3048](https://github.com/expo/eas-cli/pull/3048) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- Removed repack support. ([#3044](https://github.com/expo/eas-cli/pull/3044) by [@kudo](https://github.com/kudo))
## [16.9.0](https://github.com/expo/eas-cli/releases/tag/v16.9.0) - 2025-06-06
### 🎉 New features
- Add `groups` option to the `submit` command. ([#2891](https://github.com/expo/eas-cli/pull/2891) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
- [eas-cli] Ensure workflow:runs returns most recent runs. ([#3045](https://github.com/expo/eas-cli/pull/3045) by [@douglowder](https://github.com/douglowder))
## [16.8.0](https://github.com/expo/eas-cli/releases/tag/v16.8.0) - 2025-06-04
### 🎉 New features
- [eas-cli] Add workflow:list and workflow:runs. ([#3030](https://github.com/expo/eas-cli/pull/3030) by [@douglowder](https://github.com/douglowder))
### 🧹 Chores
- Change location of EAS Update host override config. ([#3042](https://github.com/expo/eas-cli/pull/3042) by [@wschurman](https://github.com/wschurman))
## [16.7.2](https://github.com/expo/eas-cli/releases/tag/v16.7.2) - 2025-06-03
### 🐛 Bug fixes
- Add support for array `flavorDimensions` (`flavorDimensions = ['version']`). ([#3043](https://github.com/expo/eas-cli/pull/3043) by [@sjchmiela](https://github.com/sjchmiela))
## [16.7.1](https://github.com/expo/eas-cli/releases/tag/v16.7.1) - 2025-06-03
### 🐛 Bug fixes
- Fix some unit tests by updating mocks. ([#3035](https://github.com/expo/eas-cli/pull/3035) by [@douglowder](https://github.com/douglowder))
- Fix update republishing not including environment or git info. ([#3036](https://github.com/expo/eas-cli/pull/3036) by [@wschurman](https://github.com/wschurman))
- Do not require `eas.json` or `package.json` files when creating new workflow runs. ([#3028](https://github.com/expo/eas-cli/pull/3028) by [@sjchmiela](https://github.com/sjchmiela))
- Add `EAS_SKIP_CLI_VERSION_CHECK` allowing us to skip `eas-cli` version check (against `eas.json#version`). ([#3041](https://github.com/expo/eas-cli/pull/3041) by [@sjchmiela](https://github.com/sjchmiela))
## [16.7.0](https://github.com/expo/eas-cli/releases/tag/v16.7.0) - 2025-05-29
### 🎉 New features
- Add support for manifest host and asset host overriding for EAS Update. ([#3021](https://github.com/expo/eas-cli/pull/3021) by [@wschurman](https://github.com/wschurman))
- Add rollout flag to update:republish. ([#3029](https://github.com/expo/eas-cli/pull/3029) by [@quinlanj](https://github.com/quinlanj))
## [16.6.2](https://github.com/expo/eas-cli/releases/tag/v16.6.2) - 2025-05-23
### 🧹 Chores
- Further no longer require owner field for SDK >= 53 or canary. ([#3017](https://github.com/expo/eas-cli/pull/3017) by [@wschurman](https://github.com/wschurman))
## [16.6.1](https://github.com/expo/eas-cli/releases/tag/v16.6.1) - 2025-05-14
### 🐛 Bug fixes
- Onboarding: add cwd to spawned git process. ([#3015](https://github.com/expo/eas-cli/pull/3015) by [@quinlanj](https://github.com/quinlanj))
## [16.6.0](https://github.com/expo/eas-cli/releases/tag/v16.6.0) - 2025-05-14
### 🎉 New features
- Add support for uploading iOS internal distribution local builds. ([#3014](https://github.com/expo/eas-cli/pull/3014) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.5.0](https://github.com/expo/eas-cli/releases/tag/v16.5.0) - 2025-05-13
### 🎉 New features
- Add `--wait`/`--no-wait` flag to `eas workflow:run`. ([#3012](https://github.com/expo/eas-cli/pull/3012) by [@sjchmiela](https://github.com/sjchmiela))
- Add `--json` flag to `eas workflow:run`. ([#3013](https://github.com/expo/eas-cli/pull/3013) by [@sjchmiela](https://github.com/sjchmiela))
## [16.4.2](https://github.com/expo/eas-cli/releases/tag/v16.4.2) - 2025-05-10
### 🧹 Chores
- Update `eas-build-cache-provider` function names to use `buildCacheProvider` key. ([#3002](https://github.com/expo/eas-cli/pull/3002) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.4.1](https://github.com/expo/eas-cli/releases/tag/v16.4.1) - 2025-05-07
### 🧹 Chores
- Allow non-standard build types in custom gradle command. ([#3003](https://github.com/expo/eas-cli/pull/3003) by [@khamilowicz](https://github.com/khamilowicz))
## [16.4.0](https://github.com/expo/eas-cli/releases/tag/v16.4.0) - 2025-05-02
### 🎉 New features
- Add `--verbose-logs` flag for `build` command ([#3000](https://github.com/expo/eas-cli/pull/3000) by [@khamilowicz](https://github.com/khamilowicz))
- Add `eas-build-cache-provider` package. ([#3002](https://github.com/expo/eas-cli/pull/3002) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 🧹 Chores
- Read remote build cache provider types from `@expo/config`. ([#3005](https://github.com/expo/eas-cli/pull/3005) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## [16.3.3](https://github.com/expo/eas-cli/releases/tag/v16.3.3) - 2025-04-24
### 🐛 Bug fixes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "16.3.3",
"version": "16.14.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
@@ -1,3 +1,22 @@
# eas-build-cache-provider
A build cache provider plugin for the Expo CLI
To use the EAS remote build provider plugin, install the `eas-build-cache-provider` package as a developer dependency:
```bash
npm install --save-dev eas-build-cache-provider
```
Then, update your **app.json** to include the `buildCacheProvider` property and its provider under `experiments`:
```json
{
"expo": {
...
"experiments": {
"buildCacheProvider": "eas"
}
}
}
```
@@ -1,12 +1,12 @@
{
"name": "eas-build-cache-provider",
"description": "A build cache provider plugin for the Expo CLI",
"version": "0.1.0",
"version": "16.4.2",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/config": "10.0.6",
"@babel/code-frame": "7.23.5",
"@expo/config": "11.0.10",
"@expo/spawn-async": "^1.7.2",
"chalk": "4.1.2",
"figures": "3.2.0",
@@ -1,9 +1,6 @@
import { getPackageJson } from '@expo/config';
import { RunOptions, getPackageJson } from '@expo/config';
import { SpawnResult } from '@expo/spawn-async';
// import from '@expo/config';
type RunOptions = any;
export function isSpawnResultError(obj: any): obj is Error & SpawnResult {
return (
obj &&
+16 -27
View File
@@ -1,4 +1,9 @@
import { Platform } from '@expo/config';
import {
BuildCacheProviderPlugin,
CalculateFingerprintHashProps,
ResolveBuildCacheProps,
UploadBuildCacheProps,
} from '@expo/config';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
@@ -7,22 +12,12 @@ import path from 'path';
import { isDevClientBuild, isSpawnResultError } from './helpers';
import Log from './log';
// import from '@expo/config';
type CalculateFingerprintHashProps = any;
type RemoteBuildCachePlugin = any;
type RunOptions = any;
async function resolveRemoteBuildCacheAsync({
async function resolveBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
runOptions,
}: {
projectRoot: string;
platform: Platform;
fingerprintHash: string;
runOptions: RunOptions;
}): Promise<string | null> {
}: ResolveBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip checking for remote builds');
@@ -60,25 +55,19 @@ async function resolveRemoteBuildCacheAsync({
// @TODO(2025-04-11): remove this in a future release
if (isSpawnResultError(error) && error.stderr.includes('command build:download not found')) {
Log.warn(
`To take advantage of remote build cache, upgrade your eas-cli installation to latest.`
`To take advantage of EAS build cache provider, upgrade your eas-cli installation to latest.`
);
}
return null;
}
}
async function uploadEASRemoteBuildCacheAsync({
async function uploadBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
buildPath,
}: {
projectRoot: string;
runOptions: RunOptions;
platform: Platform;
fingerprintHash: string;
buildPath?: string;
}): Promise<string | null> {
}: UploadBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip uploading builds');
@@ -86,7 +75,7 @@ async function uploadEASRemoteBuildCacheAsync({
}
try {
Log.log(chalk`{whiteBright \u203A} {bold Uploading build to remote cache}`);
Log.log(chalk`{whiteBright \u203A} {bold Uploading build to EAS}`);
const results = await spawnAsync(
'npx',
[
@@ -138,10 +127,10 @@ async function calculateEASFingerprintHashAsync({
return null;
}
const EASRemoteBuildCacheProvider: RemoteBuildCachePlugin = {
resolveRemoteBuildCache: resolveRemoteBuildCacheAsync,
uploadRemoteBuildCache: uploadEASRemoteBuildCacheAsync,
const EASBuildCacheProvider: BuildCacheProviderPlugin = {
resolveBuildCache: resolveBuildCacheAsync,
uploadBuildCache: uploadBuildCacheAsync,
calculateFingerprintHash: calculateEASFingerprintHashAsync,
};
export default EASRemoteBuildCacheProvider;
export default EASBuildCacheProvider;
+203 -85
View File
@@ -129,9 +129,11 @@ eas --help COMMAND
* [`eas update:edit [GROUPID]`](#eas-updateedit-groupid)
* [`eas update:list`](#eas-updatelist)
* [`eas update:republish`](#eas-updaterepublish)
* [`eas update:revert-update-rollout`](#eas-updaterevert-update-rollout)
* [`eas update:roll-back-to-embedded`](#eas-updateroll-back-to-embedded)
* [`eas update:rollback`](#eas-updaterollback)
* [`eas update:view GROUPID`](#eas-updateview-groupid)
* [`eas upload`](#eas-upload)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
@@ -140,8 +142,10 @@ eas --help COMMAND
* [`eas whoami`](#eas-whoami)
* [`eas worker:alias`](#eas-workeralias)
* [`eas deploy [options]`](#eas-deploy-2moptions22m-1)
* [`eas workflow:cancel`](#eas-workflowcancel)
* [`eas workflow:create [NAME]`](#eas-workflowcreate-name)
* [`eas workflow:run [FILE]`](#eas-workflowrun-file)
* [`eas workflow:runs`](#eas-workflowruns)
* [`eas workflow:validate PATH`](#eas-workflowvalidate-path)
## `eas account:login`
@@ -162,7 +166,7 @@ ALIASES
$ eas login
```
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/account/login.ts)_
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -179,7 +183,7 @@ ALIASES
$ eas logout
```
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -196,7 +200,7 @@ ALIASES
$ eas whoami
```
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/account/view.ts)_
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -210,7 +214,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/analytics.ts)_
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -262,7 +266,7 @@ DESCRIPTION
create a branch
```
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -283,7 +287,7 @@ DESCRIPTION
delete a branch
```
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -303,7 +307,7 @@ DESCRIPTION
list all branches
```
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -323,7 +327,7 @@ DESCRIPTION
rename a branch
```
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -346,7 +350,7 @@ DESCRIPTION
view a branch
```
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -355,8 +359,8 @@ start a build
```
USAGE
$ eas build [-p android|ios|all] [-e <value>] [--local] [--output <value>] [--wait] [--clear-cache] [-s |
--auto-submit-with-profile <value>] [-m <value>] [--build-logger-level trace|debug|info|warn|error|fatal]
[--freeze-credentials] [--json --non-interactive]
--auto-submit-with-profile <value>] [--what-to-test <value>] [-m <value>] [--build-logger-level
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--verbose-logs] [--json --non-interactive]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to
@@ -377,13 +381,17 @@ FLAGS
--local Run build locally [experimental]
--non-interactive Run the command in non-interactive mode.
--output=<value> Output path for local build
--verbose-logs Use verbose logs for the build process
--[no-]wait Wait for build(s) to complete
--what-to-test=<value> Specify the "What to Test" information for the build in
TestFlight (iOS-only). To be used with the `auto-submit`
flag
DESCRIPTION
start a build
```
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/index.ts)_
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -402,7 +410,7 @@ DESCRIPTION
cancel a build
```
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -419,7 +427,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/v16.3.3/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:delete [BUILD_ID]`
@@ -438,7 +446,7 @@ DESCRIPTION
delete a build
```
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/delete.ts)_
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/delete.ts)_
## `eas build:dev`
@@ -458,7 +466,7 @@ DESCRIPTION
run dev client simulator/emulator build with matching fingerprint or create a new one
```
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/dev.ts)_
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/dev.ts)_
## `eas build:download`
@@ -479,7 +487,7 @@ DESCRIPTION
download simulator/emulator builds for a given fingerprint hash
```
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/download.ts)_
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/download.ts)_
## `eas build:inspect`
@@ -514,7 +522,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/v16.3.3/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -566,7 +574,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/v16.3.3/packages/eas-cli/src/commands/build/list.ts)_
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -595,7 +603,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -621,7 +629,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/v16.3.3/packages/eas-cli/src/commands/build/run.ts)_
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -630,11 +638,14 @@ submit app binary to App Store and/or Play Store
```
USAGE
$ eas build:submit [-p android|ios|all] [-e <value>] [--latest | --id <value> | --path <value> | --url <value>]
[--verbose] [--wait] [--verbose-fastlane] [--non-interactive]
[--what-to-test <value>] [--verbose] [--wait] [--verbose-fastlane] [-g <value>] [--non-interactive]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
-g, --groups=<value>... Internal TestFlight testing groups to add the build to (iOS only). Learn more:
https://developer.apple.com/help/app-store-connect/test-a-beta-version/add-internal-
testers
-p, --platform=(android|ios|all)
--id=<value> ID of the build to submit
--latest Submit the latest build for specified platform
@@ -644,6 +655,7 @@ FLAGS
--verbose Always print logs from EAS Submit
--verbose-fastlane Enable verbose logging for the submission process
--[no-]wait Wait for submission to complete
--what-to-test=<value> Sets the "What to test" information in TestFlight (iOS only).
DESCRIPTION
submit app binary to App Store and/or Play Store
@@ -671,7 +683,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/v16.3.3/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -690,7 +702,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/v16.3.3/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -709,7 +721,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/v16.3.3/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -726,7 +738,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/v16.3.3/packages/eas-cli/src/commands/build/view.ts)_
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -747,7 +759,7 @@ DESCRIPTION
create a channel
```
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -769,7 +781,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/v16.3.3/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -789,7 +801,7 @@ DESCRIPTION
list all channels
```
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:pause [NAME]`
@@ -811,7 +823,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/v16.3.3/packages/eas-cli/src/commands/channel/pause.ts)_
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/channel/pause.ts)_
## `eas channel:resume [NAME]`
@@ -833,7 +845,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/v16.3.3/packages/eas-cli/src/commands/channel/resume.ts)_
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/channel/resume.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -866,7 +878,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/v16.3.3/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -889,7 +901,7 @@ DESCRIPTION
view a channel
```
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -910,7 +922,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/v16.3.3/packages/eas-cli/src/commands/config.ts)_
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -927,7 +939,7 @@ DESCRIPTION
manage credentials
```
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/credentials/index.ts)_
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/credentials/index.ts)_
## `eas credentials:configure-build`
@@ -945,7 +957,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/v16.3.3/packages/eas-cli/src/commands/credentials/configure-build.ts)_
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
## `eas deploy [options]`
@@ -973,7 +985,7 @@ ALIASES
$ eas worker:deploy
```
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/deploy/index.ts)_
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/deploy/index.ts)_
## `eas deploy:alias`
@@ -998,7 +1010,7 @@ ALIASES
$ eas deploy:promote
```
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/deploy/alias.ts)_
_See code: [packages/eas-cli/src/commands/deploy/alias.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/deploy/alias.ts)_
## `eas deploy:promote`
@@ -1035,7 +1047,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/v16.3.3/packages/eas-cli/src/commands/device/create.ts)_
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -1055,7 +1067,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/v16.3.3/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -1076,7 +1088,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/v16.3.3/packages/eas-cli/src/commands/device/list.ts)_
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -1097,7 +1109,7 @@ DESCRIPTION
rename a registered device
```
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -1111,7 +1123,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/v16.3.3/packages/eas-cli/src/commands/device/view.ts)_
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -1125,7 +1137,7 @@ DESCRIPTION
display environment info
```
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas env:create [ENVIRONMENT]`
@@ -1154,7 +1166,7 @@ DESCRIPTION
create an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/create.ts)_
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/create.ts)_
## `eas env:delete [ENVIRONMENT]`
@@ -1178,7 +1190,7 @@ DESCRIPTION
delete an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/delete.ts)_
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/delete.ts)_
## `eas env:exec ENVIRONMENT BASH_COMMAND`
@@ -1199,7 +1211,7 @@ DESCRIPTION
execute a command with environment variables from the selected environment
```
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/exec.ts)_
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/exec.ts)_
## `eas env:get [ENVIRONMENT]`
@@ -1224,7 +1236,7 @@ DESCRIPTION
view an environment variable for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/get.ts)_
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/get.ts)_
## `eas env:list [ENVIRONMENT]`
@@ -1249,7 +1261,7 @@ DESCRIPTION
list environment variables for the current project or account
```
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/list.ts)_
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/list.ts)_
## `eas env:pull [ENVIRONMENT]`
@@ -1272,7 +1284,7 @@ DESCRIPTION
pull environment variables for the selected environment to .env file
```
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/pull.ts)_
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/pull.ts)_
## `eas env:push [ENVIRONMENT]`
@@ -1293,7 +1305,7 @@ DESCRIPTION
push environment variables from .env file to the selected environment
```
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/env/push.ts)_
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/push.ts)_
## `eas env:update [ENVIRONMENT]`
@@ -1324,7 +1336,7 @@ 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/v16.3.3/packages/eas-cli/src/commands/env/update.ts)_
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/env/update.ts)_
## `eas fingerprint:compare [HASH1] [HASH2]`
@@ -1369,7 +1381,7 @@ EXAMPLES
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
```
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/fingerprint/compare.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
## `eas fingerprint:generate`
@@ -1400,7 +1412,7 @@ EXAMPLES
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
```
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/fingerprint/generate.ts)_
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
## `eas help [COMMAND]`
@@ -1508,7 +1520,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -1525,7 +1537,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/v16.3.3/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -1542,7 +1554,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/v16.3.3/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1572,7 +1584,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/v16.3.3/packages/eas-cli/src/commands/open.ts)_
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1586,7 +1598,7 @@ DESCRIPTION
information about the current project
```
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/project/info.ts)_
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1609,7 +1621,7 @@ ALIASES
$ eas init
```
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/project/init.ts)_
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
@@ -1627,7 +1639,7 @@ ALIASES
$ eas onboarding
```
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/project/onboarding.ts)_
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/project/onboarding.ts)_
## `eas submit`
@@ -1636,11 +1648,14 @@ submit app binary to App Store and/or Play Store
```
USAGE
$ eas submit [-p android|ios|all] [-e <value>] [--latest | --id <value> | --path <value> | --url <value>]
[--verbose] [--wait] [--verbose-fastlane] [--non-interactive]
[--what-to-test <value>] [--verbose] [--wait] [--verbose-fastlane] [-g <value>] [--non-interactive]
FLAGS
-e, --profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
eas.json.
-g, --groups=<value>... Internal TestFlight testing groups to add the build to (iOS only). Learn more:
https://developer.apple.com/help/app-store-connect/test-a-beta-version/add-internal-
testers
-p, --platform=(android|ios|all)
--id=<value> ID of the build to submit
--latest Submit the latest build for specified platform
@@ -1650,6 +1665,7 @@ FLAGS
--verbose Always print logs from EAS Submit
--verbose-fastlane Enable verbose logging for the submission process
--[no-]wait Wait for submission to complete
--what-to-test=<value> Sets the "What to test" information in TestFlight (iOS only).
DESCRIPTION
submit app binary to App Store and/or Play Store
@@ -1658,7 +1674,7 @@ ALIASES
$ eas build:submit
```
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/submit.ts)_
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1700,7 +1716,7 @@ DESCRIPTION
publish an update group
```
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/index.ts)_
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1720,7 +1736,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1741,7 +1757,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/v16.3.3/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:edit [GROUPID]`
@@ -1765,7 +1781,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/v16.3.3/packages/eas-cli/src/commands/update/edit.ts)_
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/edit.ts)_
## `eas update:list`
@@ -1787,7 +1803,7 @@ DESCRIPTION
view the recent updates
```
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/list.ts)_
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1796,8 +1812,8 @@ roll back to an existing update
```
USAGE
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [--destination-channel <value> |
--destination-branch <value>] [-m <value>] [-p android|ios|all] [--private-key-path <value>] [--json
--non-interactive]
--destination-branch <value>] [-m <value>] [-p android|ios|all] [--private-key-path <value>] [--rollout-percentage
<value>] [--json --non-interactive]
FLAGS
-m, --message=<value> Short message describing the republished update group
@@ -1814,12 +1830,43 @@ FLAGS
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory. Only relevant if you are using code signing:
https://docs.expo.dev/eas-update/code-signing/
--rollout-percentage=<value> Percentage of users this update should be immediately available to. Users not in the
rollout will be served the previous latest update on the branch, even if that update
is itself being rolled out. The specified number must be an integer between 1 and
100. When not specified, this defaults to 100.
DESCRIPTION
roll back to an existing update
```
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:revert-update-rollout`
revert a rollout update for a project
```
USAGE
$ eas update:revert-update-rollout [--channel <value> | --branch <value> | --group <value>] [-m <value>] [--private-key-path
<value>] [--json --non-interactive]
FLAGS
-m, --message=<value> Short message describing the revert
--branch=<value> Branch name to select an update group to revert the rollout update from
--channel=<value> Channel name to select an update group to revert the rollout update from
--group=<value> Rollout update group ID to revert
--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/
DESCRIPTION
revert a rollout update for a project
```
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
## `eas update:roll-back-to-embedded`
@@ -1847,7 +1894,7 @@ DESCRIPTION
roll back to the embedded update
```
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
@@ -1867,7 +1914,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/v16.3.3/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/rollback.ts)_
## `eas update:view GROUPID`
@@ -1887,7 +1934,28 @@ DESCRIPTION
update group details
```
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/update/view.ts)_
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas upload`
upload a local build and generate a sharable link
```
USAGE
$ eas upload [-p ios|android] [--build-path <value>] [--fingerprint <value>] [--json --non-interactive]
FLAGS
-p, --platform=(ios|android)
--build-path=<value> Path for the local build
--fingerprint=<value> Fingerprint hash of the local build
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
upload a local build and generate a sharable link
```
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/upload.ts)_
## `eas webhook:create`
@@ -1908,7 +1976,7 @@ DESCRIPTION
create a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1928,7 +1996,7 @@ DESCRIPTION
delete a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1946,7 +2014,7 @@ DESCRIPTION
list webhooks
```
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1968,7 +2036,7 @@ DESCRIPTION
update a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1985,7 +2053,7 @@ DESCRIPTION
view a webhook
```
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -2051,6 +2119,24 @@ ALIASES
$ eas worker:deploy
```
## `eas workflow:cancel`
Cancel one or more workflow runs. If no workflow run IDs are provided, you will be prompted to select IN_PROGRESS runs to cancel.
```
USAGE
$ eas workflow:cancel [--non-interactive]
FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
Cancel one or more workflow runs. If no workflow run IDs are provided, you will be prompted to select IN_PROGRESS runs
to cancel.
```
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
## `eas workflow:create [NAME]`
create a new workflow configuration YAML file
@@ -2069,27 +2155,59 @@ 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/v16.3.3/packages/eas-cli/src/commands/workflow/create.ts)_
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/workflow/create.ts)_
## `eas workflow:run [FILE]`
Run an EAS workflow
run an EAS workflow
```
USAGE
$ eas workflow:run [FILE] [--non-interactive]
$ eas workflow:run [FILE] [--non-interactive] [--wait] [--json]
ARGUMENTS
FILE Path to the workflow file to run
FLAGS
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--[no-]wait Wait for workflow run to complete
DESCRIPTION
Run an EAS workflow
run an EAS workflow
FLAG DESCRIPTIONS
--[no-]wait Wait for workflow run to complete
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
```
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/workflow/run.ts)_
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/workflow/run.ts)_
## `eas workflow:runs`
list recent workflow runs for this project, with their IDs, statuses, and timestamps
```
USAGE
$ eas workflow:runs [--workflow <value>] [--status ACTION_REQUIRED|CANCELED|FAILURE|IN_PROGRESS|NEW|SUCCESS]
[--json] [--limit <value>]
FLAGS
--json Enable JSON output, non-JSON messages will be
printed to stderr.
--limit=<value> The number of items to fetch each query. Defaults
to 10 and is capped at 100.
--status=(ACTION_REQUIRED|CANCELED|FAILURE|IN_PROGRESS|NEW|SUCCESS) If present, filter the returned runs to select
those with the specified status
--workflow=<value> If present, the query will only return runs for
the specified workflow file name
DESCRIPTION
list recent workflow runs for this project, with their IDs, statuses, and timestamps
```
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/workflow/runs.ts)_
## `eas workflow:validate PATH`
@@ -2109,5 +2227,5 @@ DESCRIPTION
validate a workflow configuration yaml file
```
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/workflow/validate.ts)_
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v16.14.0/packages/eas-cli/src/commands/workflow/validate.ts)_
<!-- commandsstop -->
+1 -7
View File
@@ -1,13 +1,7 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
- 'src/worker/**/*.ts'
- 'src/**/!(*.d).ts'
generates:
src/graphql/generated.ts:
plugins:
+1629 -493
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "16.3.3",
"version": "16.14.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -13,7 +13,7 @@
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.173",
"@expo/eas-json": "16.3.3",
"@expo/eas-json": "16.13.2",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.117",
@@ -38,6 +38,7 @@
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"bplist-parser": "^0.3.0",
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"dateformat": "4.6.3",
@@ -233,6 +234,7 @@
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"verify-graphql-code": "./scripts/verify-graphql.sh",
"clean": "rimraf dist build tmp node_modules yarn-error.log"
},
"volta": {
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
function cleanup()
{
echo 'Cleaning up...'
rm -f graphql.schema.json.bak src/graphql/generated.ts.bak
}
# Fail if anything errors
set -eox pipefail
# If this script exits, trap it first and clean up
trap cleanup EXIT
cp graphql.schema.json graphql.schema.json.bak
cp src/graphql/generated.ts src/graphql/generated.ts.bak
yarn generate-graphql-code
if cmp -s graphql.schema.json graphql.schema.json.bak; then
echo "GraphQL schema is up-to-date"
if cmp -s src/graphql/generated.ts src/graphql/generated.ts.bak; then
echo "GraphQL generated code is up-to-date"
exit 0
else
echo "GraphQL code has changed but has not been regenerated. Run `yarn generate-graphql-code` and commit the changes."
exit 1
fi
else
echo "GraphQL schema has changed on the server. Running `yarn tsc` to check for breaking changes..."
yarn tsc
# If there are no errors, then the schema is backwards compatible
exit 0
fi
@@ -7,7 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import { ContextInput, ContextOutput } from '../../commandUtils/EasCommand';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { AppFragment, Role } from '../../graphql/generated';
import { AppFragment, Role, WorkflowRunFragment, WorkflowRunStatus } from '../../graphql/generated';
export function getMockEasJson(): EasJson {
return {
@@ -81,7 +81,9 @@ export function mockCommandContext<
if (contextKey === 'loggedIn') {
result.loggedIn = {};
}
if (contextKey === 'projectId') {
result.projectId = overrides.projectId ?? mockProjectId;
}
if (contextKey === 'projectDir') {
result.projectDir = projectDir;
}
@@ -168,3 +170,66 @@ export function withLocalVersionSource(easJson: EasJson): EasJson {
},
};
}
export function getMockEmptyWorkflowRunsFragment(): WorkflowRunFragment[] {
return getMockWorkflowRunsFragment();
}
export function getMockWorkflowRunsFragment(
params?:
| undefined
| {
successes?: number;
failures?: number;
pending?: number;
}
): WorkflowRunFragment[] {
const { successes = 0, failures = 0, pending = 0 } = params ?? {};
const runs: any[] = [];
for (let i = 0; i < successes; i++) {
runs.push({
id: `success-${i}`,
status: WorkflowRunStatus.Success,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < failures; i++) {
runs.push({
id: `failure-${i}`,
status: WorkflowRunStatus.Failure,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < pending; i++) {
runs.push({
id: `pending-${i}`,
status: WorkflowRunStatus.InProgress,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
return runs;
}
@@ -0,0 +1,176 @@
import {
getMockEmptyWorkflowRunsFragment,
getMockWorkflowRunsFragment,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import WorkflowRunList from '../../commands/workflow/runs';
import { WorkflowRunStatus } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
jest.mock('../../build/android/version');
jest.mock('../../build/ios/version');
jest.mock('../../project/applicationIdentifier');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../graphql/queries/AppQuery');
jest.mock('../../graphql/queries/WorkflowRunQuery');
jest.mock('../../graphql/mutations/AppVersionMutation');
jest.mock('../../project/workflow');
jest.mock('../../project/android/gradleUtils');
jest.mock('../../project/ios/target');
jest.mock('../../project/ios/scheme');
jest.mock('fs');
jest.mock('../../log');
jest.mock('../../prompts');
jest.mock('../../utils/json');
describe(WorkflowRunList, () => {
afterEach(() => {
jest.clearAllMocks();
});
test('list workflow runs with default params', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
const cmd = mockTestCommand(WorkflowRunList, [], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
10
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list workflow runs with custom limit', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
const cmd = mockTestCommand(WorkflowRunList, ['--limit', '100'], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
100
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list workflow runs with specific filename', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
{
id: 'build',
status: WorkflowRunStatus.Success,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
},
},
]);
const cmd = mockTestCommand(WorkflowRunList, ['--workflow', 'build.yml'], ctx);
await cmd.run();
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'build.yml',
undefined,
10
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list only workflow runs with status FAILURE, get json output', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ failures: 1 }));
const cmd = mockTestCommand(WorkflowRunList, ['--status', 'FAILURE', '--json'], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
WorkflowRunStatus.Failure,
10
);
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
{
id: 'failure-0',
status: 'FAILURE',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
startedAt: '2022-01-01T00:00:00.000Z',
finishedAt: '2022-01-01T00:00:00.000Z',
workflowId: 'build',
workflowName: 'build',
workflowFileName: 'build.yml',
},
]);
});
test('list only workflow runs with specific filename and status FAILURE, get json output', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
{
id: 'build1',
status: WorkflowRunStatus.Failure,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
},
},
]);
const cmd = mockTestCommand(
WorkflowRunList,
['--workflow', 'build.yml', '--status', 'FAILURE', '--json'],
ctx
);
await cmd.run();
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'build.yml',
WorkflowRunStatus.Failure,
10
);
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
{
id: 'build1',
status: 'FAILURE',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
startedAt: '2022-01-01T00:00:00.000Z',
finishedAt: '2022-01-01T00:00:00.000Z',
workflowId: 'build',
workflowName: 'build',
workflowFileName: 'build.yml',
},
]);
});
});
+5 -1
View File
@@ -95,7 +95,11 @@ export function getExpoWebsiteBaseUrl(): string {
}
}
export function getEASUpdateURL(projectId: string): string {
export function getEASUpdateURL(projectId: string, manifestHostOverride: string | null): string {
if (manifestHostOverride) {
return new URL(projectId, `https://${manifestHostOverride}`).href;
}
if (process.env.EXPO_STAGING) {
return new URL(projectId, `https://staging-u.expo.dev`).href;
} else if (process.env.EXPO_LOCAL) {
@@ -0,0 +1,26 @@
import Log from '../../../log';
import { maybeWarnAboutNonStandardBuildType } from '../build';
const warn = jest.spyOn(Log, 'warn');
describe('maybeWarnAboutNonStandardBuildType', () => {
it('should not warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildRelease',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'release' });
expect(warn).not.toHaveBeenCalled();
});
it('should warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildStage',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'stage' });
expect(warn).toHaveBeenCalled();
});
});
+41 -1
View File
@@ -1,6 +1,7 @@
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import getenv from 'getenv';
import nullthrows from 'nullthrows';
import { transformJob } from './graphql';
@@ -12,7 +13,7 @@ import AndroidCredentialsProvider, {
} from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log';
import Log, { learnMore } from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
@@ -65,6 +66,10 @@ This means that it will most likely produce an AAB and you will not be able to i
ctx.vcsClient
);
if (gradleContext?.buildType) {
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: gradleContext.buildType });
}
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
}
@@ -168,3 +173,38 @@ async function ensureAndroidCredentialsAsync(
source: credentialsSource,
};
}
export function maybeWarnAboutNonStandardBuildType({
buildProfile,
buildType,
}: {
buildProfile: Pick<CommonContext<Platform.ANDROID>['buildProfile'], 'gradleCommand'>;
buildType: string;
}): void {
const shouldSuppressWarning = getenv.boolish('EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING', false);
if (shouldSuppressWarning) {
return;
}
const knownValidBuildTypes = ['debug', 'release'];
if (!knownValidBuildTypes.includes(buildType)) {
Log.warn(
`Custom gradle command "${chalk.bold(
buildProfile.gradleCommand
)}" has non-standard build type: "${chalk.bold(
buildType
)}". Expected the command to end with "${chalk.bold('...Debug')}" or "${chalk.bold(
'...Release'
)}"`
);
Log.warn(
`Ensure the command is spelled correctly and ${chalk.bold(
'build.gradle'
)} is configured correctly. To suppress this warning, set ${chalk.bold(
'EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING=true'
)}.`
);
Log.warn(learnMore('https://developer.android.com/build/build-variants#build-types'));
}
}
@@ -12,7 +12,7 @@ import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
import { BuildContext } from '../context';
interface JobData {
@@ -29,7 +29,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Android.Job> {
const username = getUsername(ctx.exp, ctx.user);
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
@@ -57,9 +57,7 @@ export async function prepareJobAsync(
: undefined;
let buildMode;
if (ctx.repack) {
buildMode = BuildMode.REPACK;
} else if (buildProfile.config) {
if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
@@ -114,11 +112,6 @@ export async function prepareJobAsync(
path: maybeCustomBuildConfigPath,
},
}),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
+1 -5
View File
@@ -500,11 +500,7 @@ async function handleSingleBuildProgressAsync(
if (build.priority !== BuildPriority.High) {
Log.newLine();
Log.log('Start builds sooner in the priority queue.');
Log.log(
`Sign up for EAS Production or Enterprise at ${link(
formatAccountBillingUrl(accountName)
)}`
);
Log.log(`Sign up for a paid plan at ${link(formatAccountBillingUrl(accountName))}`);
}
Log.newLine();
+1 -1
View File
@@ -62,7 +62,7 @@ export interface BuildContext<T extends Platform> {
requiredPackageManager: NodePackageManager['name'] | null;
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
isVerboseLoggingEnabled: boolean;
whatToTest?: string;
env: Record<string, string>;
}
+3 -3
View File
@@ -42,8 +42,8 @@ export async function createBuildContextAsync<T extends Platform>({
customBuildConfigMetadata,
buildLoggerLevel,
freezeCredentials,
repack,
isVerboseLoggingEnabled,
whatToTest,
env,
}: {
buildProfileName: string;
@@ -65,8 +65,8 @@ export async function createBuildContextAsync<T extends Platform>({
customBuildConfigMetadata?: CustomBuildConfigMetadata;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled: boolean;
whatToTest?: string;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
@@ -152,9 +152,9 @@ export async function createBuildContextAsync<T extends Platform>({
developmentClient,
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
isVerboseLoggingEnabled,
env,
whatToTest,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
+3 -10
View File
@@ -14,7 +14,7 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
import { BuildContext } from '../context';
interface JobData {
@@ -32,7 +32,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Ios.Job> {
const username = getUsername(ctx.exp, ctx.user);
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
@@ -49,9 +49,7 @@ export async function prepareJobAsync(
: undefined;
let buildMode;
if (ctx.repack) {
buildMode = BuildMode.REPACK;
} else if (buildProfile.config) {
if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
@@ -109,11 +107,6 @@ export async function prepareJobAsync(
path: maybeCustomBuildConfigPath,
},
}),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
+5 -2
View File
@@ -9,7 +9,10 @@ import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios
import { LocalBuildMode } from './local';
import { BuildDistributionType } from './types';
import Log from '../log';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import {
getUsernameForBuildMetadataAndBuildJob,
isExpoUpdatesInstalled,
} from '../project/projectUtils';
import { readChannelSafelyAsync as readAndroidChannelSafelyAsync } from '../update/android/UpdatesModule';
import { readChannelSafelyAsync as readIosChannelSafelyAsync } from '../update/ios/UpdatesModule';
import { easCliVersion } from '../utils/easCli';
@@ -48,7 +51,7 @@ export async function collectMetadataAsync<T extends Platform>(
ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL
? false
: await ctx.vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, ctx.user),
username: getUsernameForBuildMetadataAndBuildJob(ctx.user),
message: ctx.message,
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
@@ -97,8 +97,8 @@ export interface BuildFlags {
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled?: boolean;
whatToTest?: string;
}
export async function runBuildAndSubmitAsync({
@@ -230,6 +230,7 @@ export async function runBuildAndSubmitAsync({
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
env,
easJsonAccessor,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -272,7 +273,7 @@ export async function runBuildAndSubmitAsync({
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
)
).profile;
const submission = await prepareAndStartSubmissionAsync({
const submission = await prepareAndStartAutoSubmissionAsync({
build: startedBuild.build,
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
@@ -367,6 +368,7 @@ async function prepareAndStartBuildAsync({
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
env,
easJsonAccessor,
}: {
projectDir: string;
flags: BuildFlags;
@@ -379,6 +381,7 @@ async function prepareAndStartBuildAsync({
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
easJsonAccessor: EasJsonAccessor;
env: Env;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
@@ -401,8 +404,8 @@ async function prepareAndStartBuildAsync({
customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
whatToTest: flags.whatToTest,
env,
});
@@ -426,8 +429,18 @@ async function prepareAndStartBuildAsync({
nonInteractive: flags.nonInteractive,
buildProfile,
env: buildProfile.profile.env,
easJsonAccessor,
});
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
if (
isUsingEASUpdate(
buildCtx.exp,
buildCtx.projectId,
easJsonCliConfig.updateManifestHostOverride ?? null
)
) {
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
appId: buildCtx.projectId,
channelName: buildProfile.profile.channel,
@@ -492,7 +505,7 @@ async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragme
return await sendBuildRequestAsync();
}
async function prepareAndStartSubmissionAsync({
async function prepareAndStartAutoSubmissionAsync({
build,
buildCtx,
moreBuilds,
@@ -527,6 +540,8 @@ async function prepareAndStartSubmissionAsync({
vcsClient: buildCtx.vcsClient,
isVerboseFastlaneEnabled: false,
specifiedProfile: selectedSubmitProfileName,
groups: undefined, // use groups from submit profile
whatToTest: buildCtx.whatToTest,
});
if (moreBuilds) {
@@ -606,6 +621,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
nonInteractive,
sdkVersion,
env,
easJsonAccessor,
}: {
exp: ExpoConfig;
projectId: string;
@@ -615,6 +631,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
nonInteractive: boolean;
sdkVersion?: string;
env: Env | undefined;
easJsonAccessor: EasJsonAccessor;
}): Promise<void> {
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
return;
@@ -636,6 +653,9 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
message: `Would you like to install the "expo-updates" package and configure EAS Update now?`,
});
if (installExpoUpdates) {
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
await ensureEASUpdateIsConfiguredAsync({
exp,
projectId,
@@ -643,6 +663,7 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
platform: RequestedPlatform.All,
vcsClient,
env,
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
});
Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.');
-1
View File
@@ -35,5 +35,4 @@ export interface BuildFlags {
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
}
@@ -43,14 +43,14 @@ const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) =>
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
Log.log(`See logs: ${link(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`${appPlatformEmojis[build.platform]} ${
`See ${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} build details: ${link(logsUrl)}`
} logs: ${link(logsUrl)}`
);
});
}
@@ -18,6 +18,7 @@ export const testUpdateBranch1: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/bbe3e63a-f620-4603-8c4d-258f3e51590d',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
@@ -43,6 +44,7 @@ export const testUpdateBranch1: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/db22401b-d321-4629-a068-21d6d1f19c02',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
@@ -78,6 +80,7 @@ export const testUpdateBranch2: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/f04aafc1-0bf5-4548-8c35-726b467e7c40',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
@@ -103,6 +106,7 @@ export const testUpdateBranch2: UpdateBranchObject = {
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/d8d15786-2727-407f-bf3e-f4bda7e397d9',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
isGitWorkingTreeDirty: false,
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
@@ -1,6 +1,10 @@
import { vol } from 'memfs';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { easCliVersion } from '../../../../utils/easCli';
import {
findProjectDirAndVerifyProjectSetupAsync,
findProjectRootAsync,
} from '../findProjectDirAndVerifyProjectSetupAsync';
jest.mock('@expo/config');
jest.mock('fs');
@@ -60,3 +64,54 @@ describe(findProjectRootAsync, () => {
expect(projectRoot).toBe('/app');
});
});
describe(findProjectDirAndVerifyProjectSetupAsync, () => {
it('throws if the CLI version is not satisfied', async () => {
vol.fromJSON(
{
'./eas.json': JSON.stringify({ cli: { version: '1.0.0' } }),
'./package.json': JSON.stringify({}),
},
'/app'
);
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).rejects.toThrow(
new RegExp(
`^You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json \\(1.0.0\\)`,
's'
)
);
process.env.EAS_SKIP_CLI_VERSION_CHECK = 'true';
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).resolves.not.toThrow();
delete process.env.EAS_SKIP_CLI_VERSION_CHECK;
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).rejects.toThrow(
new RegExp(
`^You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json \\(1.0.0\\)`,
's'
)
);
});
it('does not throw if the CLI version is satisfied', async () => {
vol.fromJSON(
{
'./eas.json': JSON.stringify({ cli: { version: '>= 1.0.0' } }),
'./package.json': JSON.stringify({}),
},
'/app'
);
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).resolves.not.toThrow();
});
it('does not throw if there is no version', async () => {
vol.fromJSON(
{
'./eas.json': JSON.stringify({}),
'./package.json': JSON.stringify({}),
},
'/app'
);
await expect(findProjectDirAndVerifyProjectSetupAsync({ cwd: '/app' })).resolves.not.toThrow();
});
});
@@ -2,6 +2,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import * as PackageManagerUtils from '@expo/package-manager';
import chalk from 'chalk';
import fs from 'fs-extra';
import getenv from 'getenv';
import path from 'path';
import pkgDir from 'pkg-dir';
import semver from 'semver';
@@ -13,7 +14,14 @@ import { resolveVcsClient } from '../../../vcs';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
const shouldSkipVersionCheck = getenv.boolish('EAS_SKIP_CLI_VERSION_CHECK', false);
if (
!shouldSkipVersionCheck &&
config?.version &&
!semver.satisfies(easCliVersion, config.version)
) {
throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json (${
config.version
@@ -112,8 +120,12 @@ let ranEnsureEasCliIsNotInDependencies = false;
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
const projectDir = await findProjectRootAsync();
export async function findProjectDirAndVerifyProjectSetupAsync({
cwd,
}: {
cwd?: string;
} = {}): Promise<string> {
const projectDir = await findProjectRootAsync({ cwd });
await applyCliConfigAsync(projectDir);
if (!ranEnsureEasCliIsNotInDependencies) {
ranEnsureEasCliIsNotInDependencies = true;
@@ -180,7 +180,7 @@ export async function validateOrSetProjectIdAsync({
Log.warn('EAS project not configured.');
const getAccountNameForEASProjectSync = (exp: ExpoConfig, user: Actor): string => {
const getDefaultAccountNameForEASProject = (exp: ExpoConfig, user: Actor): string => {
if (exp.owner) {
return exp.owner;
}
@@ -191,7 +191,7 @@ export async function validateOrSetProjectIdAsync({
return user.username;
case 'Robot':
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
'Must configure EAS project by running "eas init" before using a robot user to manage the project.'
);
}
};
@@ -199,7 +199,7 @@ export async function validateOrSetProjectIdAsync({
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
graphqlClient,
{
accountName: getAccountNameForEASProjectSync(exp, actor),
accountName: getDefaultAccountNameForEASProject(exp, actor),
projectName: exp.slug,
},
{
@@ -0,0 +1,37 @@
import { WorkflowRunFragment, WorkflowRunStatus } from '../graphql/generated';
export type WorkflowRunResult = {
id: string;
status: string;
gitCommitMessage: string | null;
gitCommitHash: string | null;
startedAt: string;
finishedAt: string;
workflowId: string;
workflowName: string | null;
workflowFileName: string;
};
export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunResult[] {
return runs.map(run => {
const finishedAt = run.status === WorkflowRunStatus.InProgress ? null : run.updatedAt;
return {
id: run.id,
status: run.status,
gitCommitMessage: run.gitCommitMessage?.split('\n')[0] ?? null,
gitCommitHash: run.gitCommitHash ?? null,
startedAt: run.createdAt,
finishedAt,
workflowId: run.workflow.id,
workflowName: run.workflow.name ?? null,
workflowFileName: run.workflow.fileName,
};
});
}
export type WorkflowResult = {
id: string;
name?: string | null | undefined;
fileName: string;
createdAt: string;
updatedAt: string;
};
@@ -1,4 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -66,8 +67,13 @@ export default class BuildConfigure extends EasCommand {
nonInteractive: false,
vcsClient,
});
if (didCreateEasJson && isUsingEASUpdate(exp, projectId)) {
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
if (didCreateEasJson) {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
if (isUsingEASUpdate(exp, projectId, easJsonCliConfig.updateManifestHostOverride ?? null)) {
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
}
}
// configure expo-updates
+5 -1
View File
@@ -115,6 +115,7 @@ export default class BuildDev extends EasCommand {
projectId,
platform,
fingerprint,
profile: buildProfile.profileName,
});
if (builds.length !== 0) {
const build = builds[0];
@@ -137,6 +138,7 @@ export default class BuildDev extends EasCommand {
graphqlClient,
projectId,
platform,
profile: buildProfile.profileName,
});
if (
previousBuildsForSelectedProfile.length > 0 &&
@@ -164,7 +166,6 @@ export default class BuildDev extends EasCommand {
json: false,
autoSubmit: false,
localBuildOptions: {},
repack: false,
profile: flags.profile ?? DEFAULT_EAS_BUILD_RUN_PROFILE_NAME,
},
actor,
@@ -308,11 +309,13 @@ export default class BuildDev extends EasCommand {
projectId,
platform,
fingerprint,
profile,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: Platform;
fingerprint?: { hash: string };
profile?: string;
}): Promise<BuildFragment[]> {
return await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
@@ -323,6 +326,7 @@ export default class BuildDev extends EasCommand {
simulator: platform === Platform.IOS ? true : undefined,
distribution: platform === Platform.ANDROID ? DistributionType.Internal : undefined,
developmentClient: true,
buildProfile: profile,
},
offset: 0,
limit: 1,
+14 -8
View File
@@ -38,7 +38,7 @@ interface RawBuildFlags {
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
'verbose-logs'?: boolean;
repack: boolean;
'what-to-test'?: string;
}
export default class Build extends EasCommand {
@@ -92,6 +92,10 @@ export default class Build extends EasCommand {
helpValue: 'PROFILE_NAME',
exclusive: ['auto-submit'],
}),
'what-to-test': Flags.string({
description:
'Specify the "What to Test" information for the build in TestFlight (iOS-only). To be used with the `auto-submit` flag',
}),
'resource-class': Flags.enum({
options: Object.values(ResourceClass),
hidden: true,
@@ -114,11 +118,6 @@ export default class Build extends EasCommand {
default: false,
description: 'Prevent the build from updating credentials in non-interactive mode',
}),
repack: Flags.boolean({
default: false,
hidden: true,
description: 'Use the golden dev client build repack flow as it works for onboarding',
}),
'verbose-logs': Flags.boolean({
default: false,
description: 'Use verbose logs for the build process',
@@ -195,6 +194,13 @@ export default class Build extends EasCommand {
if (flags.json && !nonInteractive) {
Errors.error('--json is allowed only when building in non-interactive mode', { exit: 1 });
}
const autoSubmit = flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined;
if (flags['what-to-test'] && !autoSubmit) {
Errors.error(
'The --what-to-test flag can only be used with the --auto-submit or --auto-submit-with-profile flag.',
{ exit: 1 }
);
}
const requestedPlatform =
flags.platform &&
@@ -237,14 +243,14 @@ export default class Build extends EasCommand {
wait: flags['wait'],
clearCache: flags['clear-cache'],
json: flags['json'],
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
autoSubmit,
submitProfile: flags['auto-submit-with-profile'] ?? profile,
resourceClass: flags['resource-class'],
message,
buildLoggerLevel: flags['build-logger-level'],
freezeCredentials: flags['freeze-credentials'],
isVerboseLoggingEnabled: flags['verbose-logs'],
repack: flags.repack,
whatToTest: flags['what-to-test'],
};
}
@@ -121,7 +121,6 @@ export default class BuildInspect extends EasCommand {
workingdir: tmpWorkingdir,
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
},
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync,
@@ -93,7 +93,6 @@ export default class BuildInternal extends EasCommand {
localBuildMode: LocalBuildMode.INTERNAL,
},
submitProfile: flags['auto-submit-with-profile'] ?? flags.profile,
repack: false,
},
actor,
getDynamicPrivateProjectConfigAsync,
@@ -289,7 +289,7 @@ describe(EnvCreate, () => {
});
});
it('creates an account-wide variable and links it', async () => {
it('creates an account-wide variable', async () => {
const command = new EnvCreate(
[
'--name',
@@ -302,7 +302,6 @@ describe(EnvCreate, () => {
'development',
'--scope',
'account',
'--link',
],
mockConfig
);
@@ -330,18 +329,6 @@ describe(EnvCreate, () => {
},
testAccountId
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Development
);
});
it('prompts for missing arguments', async () => {
@@ -1,271 +0,0 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvLink from '../link';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvLink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
projectId,
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Linked variable ${chalk.bold('TEST_VARIABLE')} to project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('links an account-wide variable to the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvLink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('links an account-wide variable to the current project to a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvLink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('uses --variable-environment to select the variable with ambigous name', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockImplementation(
(_client, { environment }) => {
return mockVariables.filter(v => v.environments.includes(environment));
}
);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvLink(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'other-id',
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({ environments: mockVariables[0].environments });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvLink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -1,226 +0,0 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvUnlink from '../unlink';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvUnlink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
projectId,
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Unlinked variable ${chalk.bold('TEST_VARIABLE')} from project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('unlinks an account-wide variable from the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvUnlink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('unlinks an account-wide variable from the current project in a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Production],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvUnlink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvUnlink(['--variable-name', 'NON_EXISTENT_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Account-wide variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
+1 -61
View File
@@ -33,12 +33,11 @@ import {
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { isEnvironment, performForEnvironmentsAsync } from '../../utils/variableUtils';
import { isEnvironment } from '../../utils/variableUtils';
interface RawCreateFlags {
name?: string;
value?: string;
link?: boolean;
force?: boolean;
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
@@ -50,7 +49,6 @@ interface RawCreateFlags {
interface CreateFlags {
name?: string;
value?: string;
link?: boolean;
force?: boolean;
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
@@ -78,10 +76,6 @@ export default class EnvCreate extends EasCommand {
value: Flags.string({
description: 'Text value or the variable',
}),
link: Flags.boolean({
description: 'Link account-wide variable to the current project',
hidden: true, // every account-wide variable is global for now so it's not user facing
}),
force: Flags.boolean({
description: 'Overwrite existing variable',
default: false,
@@ -114,7 +108,6 @@ export default class EnvCreate extends EasCommand {
'non-interactive': nonInteractive,
environment: environments,
visibility,
link,
force,
type,
fileName,
@@ -154,29 +147,6 @@ export default class EnvCreate extends EasCommand {
});
overwrite = true;
}
if (existingVariable.scope === EnvironmentVariableScope.Shared) {
await this.promptForOverwriteAsync({
nonInteractive,
force,
message: `Account-wide variable with ${name} name already exists on this account.`,
suggestion: 'Do you want to unlink it first?',
});
Log.withTick(
`Unlinking account-wide variable ${chalk.bold(name)} on project ${chalk.bold(
projectDisplayName
)}.`
);
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
existingVariable.id,
projectId,
environment
);
});
}
}
const variable =
@@ -265,29 +235,6 @@ export default class EnvCreate extends EasCommand {
Log.withTick(
`Created a new variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
);
if (link) {
Log.withTick(
`Linking account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
variable.id,
projectId,
environment
);
});
Log.withTick(
`Linked account-wide variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
}
}
}
@@ -402,7 +349,6 @@ export default class EnvCreate extends EasCommand {
value,
environment: newEnvironments,
visibility: newVisibility,
link: rest.link ?? false,
force: rest.force ?? false,
'non-interactive': nonInteractive,
type: newType,
@@ -412,12 +358,6 @@ export default class EnvCreate extends EasCommand {
}
private sanitizeFlags(flags: RawCreateFlags): CreateFlags {
if (flags.scope !== 'account' && flags.link) {
throw new Error(
`Unexpected argument: --link can only be used when creating account-wide variables`
);
}
return {
...flags,
scope:
-204
View File
@@ -1,204 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
type LinkFlags = {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
environment?: EnvironmentVariableEnvironment[];
};
export default class EnvLink extends EasCommand {
static override description = 'link an account-wide environment variable to the current project';
// for now we only roll out global account-wide env variables so this should stay hidden
static override hidden = true;
static override flags = {
'variable-name': Flags.string({
description: 'Name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to link',
}),
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
static override args = [
{
name: 'environment',
description:
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
required: false,
},
];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvLink);
let {
'variable-name': name,
'variable-environment': currentEnvironment,
'non-interactive': nonInteractive,
environment: environments,
} = this.validateInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvLink, {
nonInteractive,
});
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment: currentEnvironment,
filterNames: name ? [name] : undefined,
});
let selectedVariable = variables[0];
if (variables.length > 1) {
if (nonInteractive) {
throw new Error(
'Multiple variables found, run command with --variable-name and --variable-environment arguments.'
);
}
selectedVariable = await selectAsync(
'Select account-wide variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
}
if (!selectedVariable) {
throw new Error(`Account-wide variable ${name} doesn't exist`);
}
let explicitSelect = false;
if (!nonInteractive && !environments) {
const selectedEnvironments =
(selectedVariable.linkedEnvironments ?? []).length > 0
? selectedVariable.linkedEnvironments
: selectedVariable.environments;
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedEnvironments ?? [],
});
explicitSelect = true;
}
if (!environments) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
);
return;
}
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
try {
if (
selectedVariable.linkedEnvironments?.includes(environment) ===
environments.includes(environment)
) {
if (!explicitSelect && environments.includes(environment)) {
Log.withTick(
`Variable ${chalk.bold(
selectedVariable.name
)} is already linked to ${environment.toLocaleLowerCase()}.`
);
}
continue;
}
if (environments.includes(environment)) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
} else if (explicitSelect) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
}
} catch (err: any) {
Log.warn(err.message);
}
}
}
private validateInputs(flags: LinkFlags, { environment }: { environment?: string }): LinkFlags {
environment = environment?.toUpperCase();
if (environment && !isEnvironment(environment)) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const environments = flags.environment
? flags.environment
: environment
? [environment as EnvironmentVariableEnvironment]
: undefined;
if (flags['non-interactive']) {
if (!flags['variable-name']) {
throw new Error(
`Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with ${chalk.bold(
'--variable-name VARIABLE_NAME'
)} flag to fix the issue`
);
}
}
return {
...flags,
environment: environments,
};
}
}
-192
View File
@@ -1,192 +0,0 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASMultiEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
type UnlinkFlags = {
'variable-name'?: string;
'non-interactive': boolean;
environment?: EnvironmentVariableEnvironment[];
};
export default class EnvUnlink extends EasCommand {
static override description =
'unlink an account-wide environment variable from the current project';
// for now we only roll out global account-wide env variables so this should stay hidden
static override hidden = true;
static override flags = {
'variable-name': Flags.string({
description: 'Name of the variable',
}),
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
static override args = [
{
name: 'environment',
description:
"Environment to unlink the variable from. One of 'production', 'preview', or 'development'.",
required: false,
},
];
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvUnlink);
let {
'variable-name': name,
'non-interactive': nonInteractive,
environment: unlinkEnvironments,
} = this.validateInputs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvUnlink, {
nonInteractive,
});
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
filterNames: name ? [name] : undefined,
});
let selectedVariable = variables[0];
if (variables.length > 1) {
if (nonInteractive) {
throw new Error("Multiple variables found, please select one using '--variable-name'");
}
selectedVariable = await selectAsync(
'Select account-wide variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
}
if (!selectedVariable) {
throw new Error(`Account-wide variable ${name} doesn't exist`);
}
let explicitSelect = false;
if (!nonInteractive && !unlinkEnvironments) {
const selectedEnvironments =
(selectedVariable.linkedEnvironments ?? []).length > 0
? selectedVariable.linkedEnvironments
: selectedVariable.environments;
const environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedEnvironments ?? [],
});
explicitSelect = true;
unlinkEnvironments = Object.values(EnvironmentVariableEnvironment).filter(
env => !environments.includes(env)
);
}
if (!unlinkEnvironments) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
);
return;
}
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
if (
selectedVariable.linkedEnvironments?.includes(environment) !==
unlinkEnvironments.includes(environment)
) {
if (!explicitSelect && unlinkEnvironments.includes(environment)) {
Log.withTick(
`Variable ${chalk.bold(
selectedVariable.name
)} is already unlinked from ${environment.toLocaleLowerCase()}.`
);
}
continue;
}
if (unlinkEnvironments.includes(environment)) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
} else if (explicitSelect) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
}
}
}
private validateInputs(
flags: UnlinkFlags,
{ environment }: { environment?: string }
): UnlinkFlags {
if (flags['non-interactive']) {
if (!flags['variable-name']) {
throw new Error(
'Current name is required in non-interactive mode. Run the command with --variable-name flag.'
);
}
}
if (environment) {
environment = environment.toUpperCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return {
environment: [environment],
...flags,
};
}
return flags;
}
}
@@ -307,7 +307,6 @@ export default class Onboarding extends EasCommand {
autoSubmit: false,
localBuildOptions: {},
freezeCredentials: false,
repack: true,
},
actor,
// eslint-disable-next-line async-protect/async-suffix
@@ -401,7 +400,7 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({
const bundleIdentifier = `com.${userPrefix}${stripInvalidCharactersForBundleIdentifier(
app.ownerAccount.name
)}.${slugPrefix}${stripInvalidCharactersForBundleIdentifier(app.slug)}`;
const updateUrl = getEASUpdateURL(app.id);
const updateUrl = getEASUpdateURL(app.id, /* manifestHostOverride */ null);
const easBuildGitHubConfig = {
android: {
+19
View File
@@ -26,6 +26,7 @@ import { maybeWarnAboutEasOutagesAsync } from '../utils/statuspageService';
interface RawCommandFlags {
platform?: string;
profile?: string;
'what-to-test'?: string;
latest?: boolean;
id?: string;
path?: string;
@@ -34,16 +35,19 @@ interface RawCommandFlags {
wait: boolean;
'non-interactive': boolean;
'verbose-fastlane': boolean;
groups?: string[];
}
interface CommandFlags {
requestedPlatform: RequestedPlatform;
profile?: string;
whatToTest?: string;
archiveFlags: SubmitArchiveFlags;
verbose: boolean;
wait: boolean;
nonInteractive: boolean;
isVerboseFastlaneEnabled: boolean;
groups?: string[];
}
export default class Submit extends EasCommand {
@@ -76,6 +80,9 @@ export default class Submit extends EasCommand {
description: 'App archive url',
exclusive: ['latest', 'id', 'path'],
}),
'what-to-test': Flags.string({
description: `Sets the "What to test" information in TestFlight (iOS only).`,
}),
verbose: Flags.boolean({
description: 'Always print logs from EAS Submit',
default: false,
@@ -89,6 +96,12 @@ export default class Submit extends EasCommand {
default: false,
description: 'Enable verbose logging for the submission process',
}),
groups: Flags.string({
description:
'Internal TestFlight testing groups to add the build to (iOS only). Learn more: https://developer.apple.com/help/app-store-connect/test-a-beta-version/add-internal-testers',
multiple: true,
char: 'g',
}),
'non-interactive': Flags.boolean({
default: false,
description: 'Run command in non-interactive mode',
@@ -140,12 +153,14 @@ export default class Submit extends EasCommand {
archiveFlags: flagsWithPlatform.archiveFlags,
nonInteractive: flagsWithPlatform.nonInteractive,
isVerboseFastlaneEnabled: flagsWithPlatform.isVerboseFastlaneEnabled,
groups: flagsWithPlatform.groups,
actor,
graphqlClient,
analytics,
exp,
projectId,
vcsClient,
whatToTest: flagsWithPlatform.whatToTest,
specifiedProfile: flagsWithPlatform.profile,
});
@@ -184,6 +199,8 @@ export default class Submit extends EasCommand {
profile,
'non-interactive': nonInteractive,
'verbose-fastlane': isVerboseFastlaneEnabled,
groups,
'what-to-test': whatToTest,
...archiveFlags
} = flags;
@@ -204,7 +221,9 @@ export default class Submit extends EasCommand {
wait,
profile,
nonInteractive,
whatToTest,
isVerboseFastlaneEnabled,
groups,
};
}
@@ -96,6 +96,7 @@ export default class SubmitInternal extends EasCommand {
projectId,
vcsClient,
specifiedProfile: flags.profile,
groups: undefined, // use groups from submit profile
});
let config;
@@ -114,6 +115,7 @@ export default class SubmitInternal extends EasCommand {
const configInput: z.input<typeof SubmissionConfig.Ios.SchemaZ> = {
ascAppIdentifier: iosConfig.ascAppIdentifier,
isVerboseFastlaneEnabled: iosConfig.isVerboseFastlaneEnabled ?? undefined,
groups: iosConfig.groups ?? undefined,
...(ascApiKeyJson
? { ascApiJsonKey: ascApiKeyJson }
: {
@@ -35,6 +35,7 @@ const updateStub: UpdateFragment = {
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
isGitWorkingTreeDirty: false,
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
@@ -32,6 +32,7 @@ const updateStub: UpdateFragment = {
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
isGitWorkingTreeDirty: false,
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
@@ -32,6 +32,7 @@ const updateStub: UpdateFragment = {
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
isGitWorkingTreeDirty: false,
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
@@ -1,3 +1,4 @@
import { EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
@@ -47,6 +48,10 @@ export default class UpdateConfigure extends EasCommand {
await vcsClient.ensureRepoExistsAsync();
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
await ensureEASUpdateIsConfiguredAsync({
exp,
projectId,
@@ -55,6 +60,7 @@ export default class UpdateConfigure extends EasCommand {
vcsClient,
env: undefined,
forceNativeConfigSync: true,
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
+4 -41
View File
@@ -1,50 +1,13 @@
import chalk from 'chalk';
import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
BackgroundJobReceiptDataFragment,
ScheduleUpdateGroupDeletionMutation,
ScheduleUpdateGroupDeletionMutationVariables,
} from '../../graphql/generated';
import { BackgroundJobReceiptNode } from '../../graphql/types/BackgroundJobReceipt';
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { scheduleUpdateGroupDeletionAsync } from '../../update/delete';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
async function scheduleUpdateGroupDeletionAsync(
graphqlClient: ExpoGraphqlClient,
{
group,
}: {
group: string;
}
): Promise<BackgroundJobReceiptDataFragment> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<ScheduleUpdateGroupDeletionMutation, ScheduleUpdateGroupDeletionMutationVariables>(
gql`
mutation ScheduleUpdateGroupDeletion($group: ID!) {
update {
scheduleUpdateGroupDeletion(group: $group) {
id
...BackgroundJobReceiptData
}
}
}
${BackgroundJobReceiptNode}
`,
{ group }
)
.toPromise()
);
return result.update.scheduleUpdateGroupDeletion;
}
export default class UpdateDelete extends EasCommand {
static override description = 'delete all the updates in an update group';
@@ -79,7 +42,7 @@ export default class UpdateDelete extends EasCommand {
}
if (!nonInteractive) {
const shouldAbort = await confirmAsync({
const shouldContinue = await confirmAsync({
message:
`🚨${chalk.red('CAUTION')}🚨\n\n` +
`${chalk.yellow(`This will delete all of the updates in group "${group}".`)} ${chalk.red(
@@ -87,10 +50,10 @@ export default class UpdateDelete extends EasCommand {
)}\n\n` +
`If you want to revert to a previous publish, you should use 'update --republish' targeted at the last working update group instead.\n\n` +
`An update group should only be deleted in an emergency like an accidental publish of a secret. In this case user 'update --republish' to revert to the last working update group first and then proceed with the deletion. Deleting an update group when it is the latest publish can lead to inconsistent caching behavior by clients.\n\n` +
`Would you like to abort?`,
`Would you like to continue?`,
});
if (shouldAbort) {
if (!shouldContinue) {
Log.log('Aborted.');
return;
}
@@ -1,4 +1,5 @@
import { Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -211,6 +212,10 @@ export default class UpdatePublish extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
await ensureEASUpdateIsConfiguredAsync({
exp: expPossiblyWithoutEasUpdateConfigured,
platform: requestedPlatform,
@@ -218,6 +223,7 @@ export default class UpdatePublish extends EasCommand {
projectId,
vcsClient,
env: undefined,
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
});
const { exp } = await getDynamicPublicProjectConfigAsync();
@@ -507,6 +513,8 @@ export default class UpdatePublish extends EasCommand {
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
environment: environment ?? null,
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
assetHostOverride: easJsonCliConfig.updateAssetHostOverride ?? null,
};
}
);
+22 -184
View File
@@ -1,20 +1,18 @@
import { Platform } from '@expo/config';
import { Flags } from '@oclif/core';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { UpdateToRepublish, republishAsync } from '../../update/republish';
import { truncateString as truncateUpdateMessage } from '../../update/utils';
import {
UpdateToRepublish,
getOrAskUpdateMessageAsync,
getUpdateGroupOrAskForUpdateGroupAsync,
republishAsync,
} from '../../update/republish';
import { getCodeSigningInfoAsync } from '../../utils/code-signing';
import { enableJsonOutput } from '../../utils/json';
@@ -31,6 +29,7 @@ type UpdateRepublishRawFlags = {
'private-key-path'?: string;
'non-interactive': boolean;
json?: boolean;
'rollout-percentage'?: number;
};
type UpdateRepublishFlags = {
@@ -44,6 +43,7 @@ type UpdateRepublishFlags = {
privateKeyPath?: string;
nonInteractive: boolean;
json: boolean;
rolloutPercentage?: number;
};
export default class UpdateRepublish extends EasCommand {
@@ -86,6 +86,12 @@ export default class UpdateRepublish 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,
}),
'rollout-percentage': Flags.integer({
description: `Percentage of users this update should be immediately available to. Users not in the rollout will be served the previous latest update on the branch, even if that update is itself being rolled out. The specified number must be an integer between 1 and 100. When not specified, this defaults to 100.`,
required: false,
min: 0,
max: 100,
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -112,7 +118,11 @@ export default class UpdateRepublish extends EasCommand {
const codeSigningInfo = await getCodeSigningInfoAsync(exp, flags.privateKeyPath);
const existingUpdates = await getOrAskUpdatesAsync(graphqlClient, projectId, flags);
const existingUpdates = await getUpdateGroupOrAskForUpdateGroupAsync(
graphqlClient,
projectId,
flags
);
const updatesToPublish = existingUpdates.filter(update =>
flags.platform.includes(update.platform as Platform)
);
@@ -130,7 +140,7 @@ export default class UpdateRepublish extends EasCommand {
);
}
if (rawFlags.platform === 'all') {
if (rawFlags.platform !== 'all') {
Log.withTick(`The republished update group will appear only on: ${rawFlags.platform}`);
} else {
const platformsFromUpdates = updatesToPublish.map(update => update.platform);
@@ -163,6 +173,7 @@ export default class UpdateRepublish extends EasCommand {
updateMessage,
codeSigningInfo,
json: flags.json,
rolloutPercentage: flags.rolloutPercentage,
});
}
@@ -191,6 +202,7 @@ export default class UpdateRepublish extends EasCommand {
platform,
updateMessage: rawFlags.message,
privateKeyPath,
rolloutPercentage: rawFlags['rollout-percentage'],
json: rawFlags.json ?? false,
nonInteractive,
};
@@ -224,177 +236,3 @@ async function getOrAskTargetBranchAsync(
// if neither provided, assume republish on same branch
return { branchId: arbitraryUpdate.branchId, branchName: arbitraryUpdate.branchName };
}
/** Retrieve the update group from either the update group id, or select from branch name. */
async function getOrAskUpdatesAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
flags: UpdateRepublishFlags
): Promise<UpdateToRepublish[]> {
if (flags.groupId) {
const updateGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: flags.groupId,
});
return updateGroup.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
}));
}
if (flags.nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
if (flags.branchName) {
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
branchName: flags.branchName,
projectId,
});
}
if (flags.channelName) {
return await askUpdatesFromChannelNameAsync(graphqlClient, {
...flags,
channelName: flags.channelName,
projectId,
});
}
const { choice } = await promptAsync({
type: 'select',
message: 'Find update by branch or channel?',
name: 'choice',
choices: [
{ title: 'Branch', value: 'branch' },
{ title: 'Channel', value: 'channel' },
],
});
if (choice === 'channel') {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel to view',
paginatedQueryOptions: {
json: flags.json,
nonInteractive: flags.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromChannelNameAsync(graphqlClient, {
...flags,
channelName: name,
projectId,
});
} else if (choice === 'branch') {
const { name } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Select branch from which to choose update',
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: flags.json,
nonInteractive: flags.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
branchName: name,
projectId,
});
} else {
throw new Error('Must choose update via channel or branch');
}
}
/** Ask the user which update needs to be republished by branch name, this requires interactive mode */
async function askUpdatesFromBranchNameAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
branchName,
json,
nonInteractive,
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
const updateGroup = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions: getPaginatedQueryOptions({ json, 'non-interactive': nonInteractive }),
});
return updateGroup.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
}));
}
/** Ask the user which update needs to be republished by channel name, this requires interactive mode */
async function askUpdatesFromChannelNameAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
channelName,
json,
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
const { branchName } = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
channelName
);
return await askUpdatesFromBranchNameAsync(graphqlClient, {
projectId,
branchName,
json,
nonInteractive,
});
}
/** Get or ask the user for the update (group) message for the republish */
async function getOrAskUpdateMessageAsync(
updates: UpdateToRepublish[],
flags: UpdateRepublishFlags
): Promise<string> {
if (flags.updateMessage) {
return sanitizeUpdateMessage(flags.updateMessage);
}
if (flags.nonInteractive || flags.json) {
throw new Error('Must supply --message when in non-interactive mode');
}
// This command only uses a single update group to republish, meaning these values are always identical
const oldGroupId = updates[0].groupId;
const oldUpdateMessage = updates[0].message;
const { updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: 'Provide an update message.',
initial: `Republish "${oldUpdateMessage!}" - group: ${oldGroupId}`,
validate: (value: any) => (value ? true : 'Update message may not be empty.'),
});
return sanitizeUpdateMessage(updateMessage);
}
function sanitizeUpdateMessage(updateMessage: string): string {
if (updateMessage !== truncateUpdateMessage(updateMessage, 1024)) {
Log.warn('Update message exceeds the allowed 1024 character limit, truncated update message.');
return truncateUpdateMessage(updateMessage, 1024);
}
return updateMessage;
}
@@ -0,0 +1,416 @@
import { ExpoConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import assert from 'assert';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import Log, { learnMore } from '../../log';
import { enforceRollBackToEmbeddedUpdateSupportAsync } from '../../project/projectUtils';
import { UpdatePublishPlatform, getUpdateMessageForCommandAsync } from '../../project/publish';
import { confirmAsync, promptAsync } from '../../prompts';
import { scheduleUpdateGroupDeletionAsync } from '../../update/delete';
import {
UpdateToRepublish,
askUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionAsync,
getOrAskUpdateMessageAsync,
getUpdateGroupAsync,
republishAsync,
} from '../../update/republish';
import { publishRollBackToEmbeddedUpdateAsync } from '../../update/roll-back-to-embedded';
import { CodeSigningInfo, getCodeSigningInfoAsync } from '../../utils/code-signing';
import { enableJsonOutput } from '../../utils/json';
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
import { Client } from '../../vcs/vcs';
type RolloutUpdate = UpdateToRepublish & {
rolloutPercentage: NonNullable<UpdateToRepublish['rolloutPercentage']>;
};
type RolloutUpdateWithControlUpdate = RolloutUpdate & {
rolloutControlUpdate: NonNullable<UpdateToRepublish['rolloutControlUpdate']>;
};
type UpdateRevertUpdateRolloutRawFlags = {
branch?: string;
channel?: string;
group?: string;
message?: string;
'private-key-path'?: string;
'non-interactive': boolean;
json?: boolean;
};
type UpdateRevertUpdateRolloutFlags = {
branchName?: string;
channelName?: string;
groupId?: string;
updateMessage?: string;
privateKeyPath?: string;
nonInteractive: boolean;
json: boolean;
};
export function nonNullish<TValue>(value: TValue | null | undefined): value is NonNullable<TValue> {
return value !== null && value !== undefined;
}
export default class UpdateRevertUpdateRollout extends EasCommand {
static override description = 'revert a rollout update for a project';
static override flags = {
channel: Flags.string({
description: 'Channel name to select an update group to revert the rollout update from',
exclusive: ['branch', 'group'],
}),
branch: Flags.string({
description: 'Branch name to select an update group to revert the rollout update from',
exclusive: ['channel', 'group'],
}),
group: Flags.string({
description: 'Rollout update group ID to revert',
exclusive: ['branch', 'channel'],
}),
message: Flags.string({
char: 'm',
description: 'Short message describing the revert',
required: 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,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdateRevertUpdateRollout);
const flags = this.sanitizeFlags(rawFlags);
const {
privateProjectConfig: { exp, projectId, projectDir },
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(UpdateRevertUpdateRollout, {
nonInteractive: flags.nonInteractive,
withServerSideEnvironment: null,
});
if (flags.json) {
enableJsonOutput();
}
const codeSigningInfo = await getCodeSigningInfoAsync(exp, flags.privateKeyPath);
let updateGroupToRepublish: RolloutUpdate[];
if (flags.groupId) {
const updateGroup = await getUpdateGroupAsync(graphqlClient, flags.groupId);
if (!updateGroupIsRolloutUpdateGroup(updateGroup)) {
throw new Error(
`The update group with ID "${flags.groupId}" is not a rollout update group.`
);
}
updateGroupToRepublish = updateGroup;
} else {
const latestUpdateGroupForEachPublishPlatform =
await askUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionAsync(
graphqlClient,
projectId,
flags
);
const uniqueUpdateGroups = getUniqueUpdateGroups(
Object.values(latestUpdateGroupForEachPublishPlatform).filter(nonNullish)
);
const rolloutUpdateGroups = uniqueUpdateGroups.filter(updateGroupIsRolloutUpdateGroup);
if (rolloutUpdateGroups.length === 0) {
throw new Error(`No rollout update groups found.`);
}
if (rolloutUpdateGroups.length === 1) {
updateGroupToRepublish = rolloutUpdateGroups[0];
} else {
const { choice: chosenId } = await promptAsync({
type: 'select',
message: 'Which rollout update group would you like to revert?',
name: 'choice',
choices: rolloutUpdateGroups.map(ug => ({
title: `Rollout update group ID: ${ug[0].groupId}, Platform: ${ug[0].platform}, Rollout Percentage: ${ug[0].rolloutPercentage}`,
value: ug[0].groupId,
})),
});
if (!chosenId) {
throw new Error('No rollout update group selected.');
}
const chosenUpdateGroup = rolloutUpdateGroups.find(ug => ug[0].groupId === chosenId);
if (!chosenUpdateGroup) {
throw new Error('No rollout update group selected.');
}
updateGroupToRepublish = chosenUpdateGroup;
}
}
const rolloutUpdateGroupWithControlUpdates: RolloutUpdateWithControlUpdate[] | null =
updateGroupIsUpdateGroupWithControlUpdate(updateGroupToRepublish)
? updateGroupToRepublish
: null;
if (rolloutUpdateGroupWithControlUpdates) {
await this.deleteRolloutUpdateGroupAndRepublishControlUpdatesAsync({
graphqlClient,
exp,
projectId,
rolloutUpdateGroupWithControlUpdates,
codeSigningInfo,
flags,
});
} else {
await this.deleteRolloutUpdateGroupAndPublishRollBackToEmbeddedAsync({
graphqlClient,
vcsClient,
exp,
projectDir,
projectId,
rolloutUpdateGroup: updateGroupToRepublish,
codeSigningInfo,
flags,
});
}
}
private async deleteRolloutUpdateGroupAndRepublishControlUpdatesAsync({
graphqlClient,
exp,
projectId,
rolloutUpdateGroupWithControlUpdates,
codeSigningInfo,
flags,
}: {
graphqlClient: ExpoGraphqlClient;
exp: ExpoConfig;
projectId: string;
rolloutUpdateGroupWithControlUpdates: RolloutUpdateWithControlUpdate[];
codeSigningInfo: CodeSigningInfo | undefined;
flags: UpdateRevertUpdateRolloutFlags;
}): Promise<void> {
const controlUpdateGroupIdsToRepublish = Array.from(
new Set(rolloutUpdateGroupWithControlUpdates.map(update => update.rolloutControlUpdate.group))
);
const updateGroupsToRepublish = await Promise.all(
controlUpdateGroupIdsToRepublish.map(controlUpdateGroupIdToRepublish =>
getUpdateGroupAsync(graphqlClient, controlUpdateGroupIdToRepublish)
)
);
const updateGroupOrGroupsClause =
controlUpdateGroupIdsToRepublish.length > 1
? `control update groups (IDs: ${controlUpdateGroupIdsToRepublish
.map(id => `"${id}"`)
.join(', ')})`
: `control update group (ID: "${controlUpdateGroupIdsToRepublish[0]}")`;
if (!flags.nonInteractive) {
const confirmMessage = `Are you sure you want to revert the rollout update group with ID "${rolloutUpdateGroupWithControlUpdates[0].groupId}"? This will delete the rollout update group and republish the ${updateGroupOrGroupsClause}.`;
const didConfirm = await confirmAsync({ message: confirmMessage });
if (!didConfirm) {
throw new Error('Aborting...');
}
}
const updateMessages: string[] = [];
for (const updateGroup of updateGroupsToRepublish) {
updateMessages.push(await getOrAskUpdateMessageAsync(updateGroup, flags));
}
// assert all updateGroupsToRepublish have the same branch name and id
const branchNames = updateGroupsToRepublish.flatMap(updateGroup => updateGroup[0].branchName);
const branchIds = updateGroupsToRepublish.map(updateGroup => updateGroup[0].branchId);
assert(
branchNames.every(name => name === branchNames[0]),
'All update groups being republished must belong to the same branch.'
);
assert(
branchIds.every(id => id === branchIds[0]),
'All update groups being republished must belong to the same branch.'
);
const targetBranch = {
branchName: branchNames[0],
branchId: branchIds[0],
};
await this.deleteRolloutUpdateGroupAsync({
graphqlClient,
rolloutUpdateGroup: rolloutUpdateGroupWithControlUpdates,
});
for (let i = 0; i < updateGroupsToRepublish.length; i++) {
const updateGroupToRepublish = updateGroupsToRepublish[i];
const updateMessage = updateMessages[i];
await republishAsync({
graphqlClient,
app: { exp, projectId },
updatesToPublish: updateGroupToRepublish,
targetBranch,
updateMessage,
codeSigningInfo,
json: flags.json,
});
}
}
private async deleteRolloutUpdateGroupAndPublishRollBackToEmbeddedAsync({
graphqlClient,
vcsClient,
exp,
projectDir,
projectId,
rolloutUpdateGroup,
codeSigningInfo,
flags,
}: {
graphqlClient: ExpoGraphqlClient;
vcsClient: Client;
exp: ExpoConfig;
projectDir: string;
projectId: string;
rolloutUpdateGroup: RolloutUpdate[];
codeSigningInfo: CodeSigningInfo | undefined;
flags: UpdateRevertUpdateRolloutFlags;
}): Promise<void> {
const rolloutUpdateGroupId = rolloutUpdateGroup[0].groupId;
if (!flags.nonInteractive) {
const confirmMessage = `Are you sure you want to revert the rollout update group with ID "${rolloutUpdateGroupId}"? This will delete the rollout update group and publish a new roll-back-to-embedded update (no control update to roll back to), whose behavior may not be a true revert depending on the previous state of the branch. ${learnMore(
'https://expo.fyi/eas-update-update-rollouts',
{ learnMoreMessage: 'More info' }
)})`;
const didConfirm = await confirmAsync({ message: confirmMessage });
if (!didConfirm) {
throw new Error('Aborting...');
}
}
// check that the expo-updates package version supports roll back to embedded
await enforceRollBackToEmbeddedUpdateSupportAsync(projectDir);
const updateMessage = await getUpdateMessageForCommandAsync(vcsClient, {
updateMessageArg: flags.updateMessage,
autoFlag: false,
nonInteractive: flags.nonInteractive,
jsonFlag: flags.json,
});
await this.deleteRolloutUpdateGroupAsync({
graphqlClient,
rolloutUpdateGroup,
});
const platforms = rolloutUpdateGroup.map(update => update.platform) as UpdatePublishPlatform[];
const runtimeVersion = rolloutUpdateGroup[0].runtimeVersion;
const targetBranch = {
name: rolloutUpdateGroup[0].branchName,
id: rolloutUpdateGroup[0].branchId,
};
await publishRollBackToEmbeddedUpdateAsync({
graphqlClient,
projectId,
exp,
updateMessage,
branch: targetBranch,
codeSigningInfo,
platforms,
runtimeVersion,
json: flags.json,
});
}
private async deleteRolloutUpdateGroupAsync({
graphqlClient,
rolloutUpdateGroup,
}: {
graphqlClient: ExpoGraphqlClient;
rolloutUpdateGroup: RolloutUpdate[];
}): Promise<void> {
const rolloutUpdateGroupId = rolloutUpdateGroup[0].groupId;
const updateGroupDeletionReceipt = await scheduleUpdateGroupDeletionAsync(graphqlClient, {
group: rolloutUpdateGroupId,
});
const successfulReceipt = await pollForBackgroundJobReceiptAsync(
graphqlClient,
updateGroupDeletionReceipt
);
Log.debug('Rollout update group deletion result', { successfulReceipt });
}
private sanitizeFlags(
rawFlags: UpdateRevertUpdateRolloutRawFlags
): UpdateRevertUpdateRolloutFlags {
const branchName = rawFlags.branch;
const channelName = rawFlags.channel;
const groupId = rawFlags.group;
const nonInteractive = rawFlags['non-interactive'];
const privateKeyPath = rawFlags['private-key-path'];
if (nonInteractive && !groupId) {
throw new Error('Only --group can be used in non-interactive mode');
}
return {
branchName,
channelName,
groupId,
updateMessage: rawFlags.message,
privateKeyPath,
json: rawFlags.json ?? false,
nonInteractive,
};
}
}
function getUniqueUpdateGroups(updateGroups: UpdateToRepublish[][]): UpdateToRepublish[][] {
const uniqueUpdateGroups = new Map<string, UpdateToRepublish[]>();
for (const updateGroup of updateGroups) {
const groupId = updateGroup[0].groupId;
if (!uniqueUpdateGroups.has(groupId)) {
uniqueUpdateGroups.set(groupId, updateGroup);
}
}
return Array.from(uniqueUpdateGroups.values());
}
function updateGroupIsRolloutUpdateGroup(
updateGroup: UpdateToRepublish[]
): updateGroup is RolloutUpdate[] {
return updateGroup.every(updateIsRolloutUpdate);
}
function updateIsRolloutUpdate(updateGroup: UpdateToRepublish): updateGroup is RolloutUpdate {
return updateGroup.rolloutPercentage !== undefined && updateGroup.rolloutPercentage !== null;
}
function updateGroupIsUpdateGroupWithControlUpdate(
updateGroup: RolloutUpdate[]
): updateGroup is RolloutUpdateWithControlUpdate[] {
return updateGroup.every(updateIsRolloutWithControlUpdate);
}
function updateIsRolloutWithControlUpdate(
updateGroup: RolloutUpdate
): updateGroup is RolloutUpdateWithControlUpdate {
return !!updateGroup.rolloutControlUpdate;
}
@@ -1,50 +1,24 @@
import { Platform as PublishPlatform } from '@expo/config';
import { EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
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 { StatuspageServiceName } from '../../graphql/generated';
import { RequestedPlatform } from '../../platform';
import { enforceRollBackToEmbeddedUpdateSupportAsync } from '../../project/projectUtils';
import {
enforceRollBackToEmbeddedUpdateSupportAsync,
getOwnerAccountForProjectIdAsync,
} from '../../project/projectUtils';
import {
RuntimeVersionInfo,
UpdatePublishPlatform,
defaultPublishPlatforms,
getBranchNameForCommandAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
import {
CodeSigningInfo,
checkDirectiveBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getDirectiveBodyAsync,
signBody,
} from '../../utils/code-signing';
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 { selectRuntimeOnBranchAsync } from '../../update/queries';
import { publishRollBackToEmbeddedUpdateAsync } from '../../update/roll-back-to-embedded';
import { getCodeSigningInfoAsync } from '../../utils/code-signing';
import { enableJsonOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
type RawUpdateFlags = {
@@ -148,6 +122,10 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
await ensureEASUpdateIsConfiguredAsync({
exp: expPossiblyWithoutEasUpdateConfigured,
platform: platformFlag,
@@ -155,6 +133,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
projectId,
vcsClient,
env: undefined,
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
});
// check that the expo-updates package version supports roll back to embedded
@@ -193,7 +172,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const selectedRuntime =
runtimeVersionArg ??
(
await UpdateRollBackToEmbedded.selectRuntimeAsync(graphqlClient, {
await selectRuntimeOnBranchAsync(graphqlClient, {
appId: projectId,
branchName,
})
@@ -202,196 +181,16 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
Errors.error('Must select a runtime or provide the --runtimeVersion flag', { exit: 1 });
}
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
realizedPlatforms.map(platform => ({
platform,
runtimeVersionInfo: {
runtimeVersion: selectedRuntime,
expoUpdatesRuntimeFingerprint: null,
expoUpdatesRuntimeFingerprintHash: null,
},
}))
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await this.publishRollbacksAsync({
graphqlClient,
updateMessage,
branchId: branch.id,
codeSigningInfo,
runtimeToPlatformsAndFingerprintInfoMapping,
realizedPlatforms,
});
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to publish updates');
throw e;
}
if (jsonFlag) {
printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
} else {
Log.addNewLineIfNone();
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: updateMessage ?? '' },
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
}
}
}
private async publishRollbacksAsync({
graphqlClient,
updateMessage,
branchId,
codeSigningInfo,
runtimeToPlatformsAndFingerprintInfoMapping,
realizedPlatforms,
}: {
graphqlClient: ExpoGraphqlClient;
updateMessage: string | undefined;
branchId: string;
codeSigningInfo: CodeSigningInfo | undefined;
runtimeToPlatformsAndFingerprintInfoMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
})[];
realizedPlatforms: PublishPlatform[];
}): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
const rollbackInfoGroups = Object.fromEntries(
realizedPlatforms.map(platform => [platform, true])
);
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformsAndFingerprintInfoMapping.map(
({ runtimeVersion, platforms }) => {
const localRollbackInfoGroup = Object.fromEntries(
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
);
return {
branchId,
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
runtimeVersion,
message: updateMessage,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
const newUpdates = await PublishMutation.publishUpdateGroupAsync(graphqlClient, updateGroups);
if (codeSigningInfo) {
Log.log('🔒 Signing roll back');
const updatesTemp = [...newUpdates];
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
const newUpdates = updatesTemp.splice(
0,
Object.keys(nullthrows(updateGroup.rollBackToEmbeddedInfoGroup)).length
);
return {
updateGroup,
newUpdates,
};
});
await Promise.all(
updateGroupsAndTheirUpdates.map(async ({ newUpdates }) => {
await Promise.all(
newUpdates.map(async newUpdate => {
const response = await fetch(newUpdate.manifestPermalink, {
method: 'GET',
headers: { accept: 'multipart/mixed' },
});
const directiveBody = nullthrows(await getDirectiveBodyAsync(response));
checkDirectiveBodyAgainstUpdateInfoGroup(directiveBody);
const directiveSignature = signBody(directiveBody, codeSigningInfo);
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
keyid: codeSigningInfo.codeSigningMetadata.keyid,
sig: directiveSignature,
});
})
);
})
);
}
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,
await publishRollBackToEmbeddedUpdateAsync({
graphqlClient,
projectId,
exp,
updateMessage,
branch,
codeSigningInfo,
platforms: realizedPlatforms,
runtimeVersion: selectedRuntime,
json: jsonFlag,
});
}
+86 -8
View File
@@ -13,6 +13,7 @@ import EasCommand from '../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
import {
BuildMetadataInput,
DistributionType,
LocalBuildArchiveSourceType,
UploadSessionType,
@@ -27,11 +28,11 @@ import { uploadFileAtPathToGCSAsync } from '../uploads';
import { fromNow } from '../utils/date';
import { enableJsonOutput, printJsonOnlyOutput } from '../utils/json';
import { getTmpDirectory } from '../utils/paths';
import { parseBinaryPlistBuffer } from '../utils/plist';
import { createProgressTracker } from '../utils/progress';
export default class BuildUpload extends EasCommand {
static override description = 'upload a local build and generate a sharable link';
static override hidden = true;
static override flags = {
platform: Flags.enum<Platform.IOS | Platform.ANDROID>({
@@ -82,6 +83,7 @@ export default class BuildUpload extends EasCommand {
fingerprintHash: buildFingerprintHash,
developmentClient,
simulator,
...otherMetadata
} = await extractAppMetadataAsync(localBuildPath, platform);
let fingerprint = manualFingerprintHash ?? buildFingerprintHash;
@@ -120,7 +122,12 @@ export default class BuildUpload extends EasCommand {
projectId,
{ platform: toAppPlatform(platform), simulator },
{ type: LocalBuildArchiveSourceType.Gcs, bucketKey },
{ distribution: DistributionType.Internal, fingerprintHash: fingerprint, developmentClient }
{
distribution: DistributionType.Internal,
fingerprintHash: fingerprint,
developmentClient,
...otherMetadata,
}
);
if (jsonFlag) {
@@ -303,19 +310,33 @@ async function uploadAppArchiveAsync(
return bucketKey;
}
type AppMetadata = {
fingerprintHash?: string;
developmentClient: boolean;
function getInfoPlistMetadata(infoPlist: any): {
appName?: string;
appIdentifier?: string;
simulator: boolean;
};
} {
const appName = infoPlist?.CFBundleDisplayName ?? infoPlist?.CFBundleName;
const appIdentifier = infoPlist?.CFBundleIdentifier;
const simulator = infoPlist?.DTPlatformName?.includes('simulator');
return {
appName,
appIdentifier,
simulator,
};
}
async function extractAppMetadataAsync(
buildPath: string,
platform: Platform
): Promise<AppMetadata> {
): Promise<{ developmentClient: boolean; simulator: boolean } & BuildMetadataInput> {
let developmentClient = false;
let fingerprintHash: string | undefined;
const simulator = platform === Platform.IOS;
// By default, we assume the iOS apps are for simulators
let simulator = platform === Platform.IOS;
let appName: string | undefined;
let appIdentifier: string | undefined;
const basePath = platform === Platform.ANDROID ? 'assets/' : buildPath;
const fingerprintFilePath =
@@ -340,14 +361,51 @@ async function extractAppMetadataAsync(
}
} else if (buildExtension === '.app') {
developmentClient = await fs.exists(path.join(basePath, devMenuBundlePath));
if (await fs.exists(path.join(basePath, 'Info.plist'))) {
const infoPlistBuffer = await fs.readFile(path.join(basePath, 'Info.plist'));
const infoPlist = parseBinaryPlistBuffer(infoPlistBuffer);
({ simulator, appIdentifier, appName } = getInfoPlistMetadata(infoPlist));
}
if (await fs.exists(path.join(basePath, fingerprintFilePath))) {
fingerprintHash = await fs.readFile(path.join(basePath, fingerprintFilePath), 'utf8');
}
} else if (buildExtension === '.ipa') {
const zip = new StreamZip.async({ file: buildPath });
try {
const entries = await zip.entries();
const entriesKeys = Object.keys(entries);
await Promise.all(
entriesKeys.map(async path => {
const infoPlistRegex = /^Payload\/[^/]+\.app\/Info\.plist$/;
if (infoPlistRegex.test(path)) {
const infoPlistBuffer = await zip.entryData(entries[path]);
const infoPlist = parseBinaryPlistBuffer(infoPlistBuffer);
({ simulator, appIdentifier, appName } = getInfoPlistMetadata(infoPlist));
return;
}
if (path.includes('/EXDevMenu.bundle')) {
developmentClient = true;
return;
}
if (path.includes('EXUpdates.bundle/fingerprint')) {
fingerprintHash = (await zip.entryData(entries[path])).toString('utf-8');
}
})
);
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
} finally {
await zip.close();
}
} else {
// Use tar to list files in the archive
try {
let fingerprintHashPromise: Promise<string> | undefined;
let infoPlistPromise: Promise<Buffer> | undefined;
await tar.list({
file: buildPath,
// eslint-disable-next-line async-protect/async-suffix
@@ -368,11 +426,29 @@ async function extractAppMetadataAsync(
}
});
}
if (entry.path.endsWith('Info.plist')) {
infoPlistPromise = new Promise<Buffer>(async (resolve, reject) => {
try {
const chunks: Buffer[] = [];
for await (const chunk of entry) {
chunks.push(chunk);
}
const content = Buffer.concat(chunks);
resolve(content);
} catch (error) {
reject(error);
}
});
}
},
});
if (fingerprintHashPromise !== undefined) {
fingerprintHash = await fingerprintHashPromise;
}
if (infoPlistPromise !== undefined) {
const infoPlist = parseBinaryPlistBuffer(await infoPlistPromise);
({ simulator, appIdentifier, appName } = getInfoPlistMetadata(infoPlist));
}
} catch (err) {
Log.error(`Error reading ${buildExtension}: ${err}`);
}
@@ -382,5 +458,7 @@ async function extractAppMetadataAsync(
developmentClient,
fingerprintHash,
simulator,
appName,
appIdentifier,
};
}
@@ -0,0 +1,98 @@
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { processWorkflowRuns } from '../../commandUtils/workflows';
import { WorkflowRunStatus } from '../../graphql/generated';
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log from '../../log';
import { promptAsync } from '../../prompts';
export default class WorkflowRunCancel extends EasCommand {
static override description =
'Cancel one or more workflow runs. If no workflow run IDs are provided, you will be prompted to select IN_PROGRESS runs to cancel.';
static override strict = false;
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASNonInteractiveFlag,
};
async runAsync(): Promise<void> {
const { argv } = await this.parse(WorkflowRunCancel);
let nonInteractive = false;
const workflowRunIds: Set<string> = new Set();
// Custom parsing of argv
const tokens: string[] = [...argv];
while (tokens.length > 0) {
const token = tokens.shift();
if (token === '--non-interactive') {
nonInteractive = true;
continue;
} else if (token) {
workflowRunIds.add(token);
}
}
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkflowRunCancel, {
nonInteractive,
});
if (workflowRunIds.size === 0) {
if (nonInteractive) {
throw new Error('Must supply workflow run IDs as arguments when in non-interactive mode');
}
// Run the workflow run list query and select runs to cancel
const queryResult = await AppQuery.byIdWorkflowRunsFilteredByStatusAsync(
graphqlClient,
projectId,
WorkflowRunStatus.InProgress,
50
);
const runs = processWorkflowRuns(queryResult);
if (runs.length === 0) {
Log.warn('No workflow runs to cancel');
return;
}
const answers = await promptAsync({
type: 'multiselect',
name: 'selectedRuns',
message: 'Select IN_PROGRESS workflow runs to cancel',
choices: runs.map(run => ({
title: `${run.id} - ${run.workflowFileName}, ${run.gitCommitMessage ?? ''}, ${
run.startedAt
}`,
value: run.id,
})),
});
answers.selectedRuns.forEach((id: string) => {
workflowRunIds.add(id);
});
if (workflowRunIds.size === 0) {
Log.warn('No workflow runs to cancel');
return;
}
}
Log.addNewLineIfNone();
for (const workflowRunId of workflowRunIds) {
try {
await WorkflowRunMutation.cancelWorkflowRunAsync(graphqlClient, {
workflowRunId,
});
Log.log(`Workflow run ${workflowRunId} has been canceled.`);
} catch (e: any) {
Log.error(`Failed to cancel workflow run ${workflowRunId}: ${e}`);
}
}
Log.addNewLineIfNone();
}
}
@@ -0,0 +1,64 @@
import EasCommand from '../../commandUtils/EasCommand';
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
import { WorkflowResult } from '../../commandUtils/workflows';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log from '../../log';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class WorkflowList extends EasCommand {
static override hidden = true;
static override description = 'List workflows for the current project';
static override flags = {
...EasJsonOnlyFlag,
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(WorkflowList);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkflowList, {
nonInteractive: true,
});
if (flags.json) {
enableJsonOutput();
}
const workflows = await AppQuery.byIdWorkflowsAsync(graphqlClient, projectId);
const result: WorkflowResult[] = workflows.map(workflow => ({
id: workflow.id,
name: workflow.name,
fileName: workflow.fileName,
createdAt: workflow.createdAt,
updatedAt: workflow.updatedAt,
}));
if (flags.json) {
printJsonOnlyOutput(result);
return;
}
Log.addNewLineIfNone();
result.forEach(workflow => {
Log.log(
formatFields([
{ label: 'ID', value: workflow.id },
{ label: 'Name', value: workflow.name ?? '-' },
{ label: 'File name', value: workflow.fileName },
{ label: 'Created At', value: workflow.createdAt },
{ label: 'Updated At', value: workflow.updatedAt },
])
);
Log.addNewLineIfNone();
});
}
}
+187 -39
View File
@@ -1,25 +1,51 @@
import { Flags } from '@oclif/core';
import { CombinedError } from '@urql/core';
import chalk from 'chalk';
import * as fs from 'node:fs';
import * as path from 'node:path';
import slash from 'slash';
import { getWorkflowRunUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WorkflowProjectSourceType } from '../../graphql/generated';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
import {
WorkflowProjectSourceType,
WorkflowRunByIdQuery,
WorkflowRunStatus,
} from '../../graphql/generated';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { sleepAsync } from '../../utils/promise';
import { WorkflowFile } from '../../utils/workflowFile';
const EXIT_CODES = {
WORKFLOW_FAILED: 11,
WORKFLOW_CANCELED: 12,
WAIT_ABORTED: 13,
};
export default class WorkflowRun extends EasCommand {
static override description = 'Run an EAS workflow';
static override description = 'run an EAS workflow';
static override args = [{ name: 'file', description: 'Path to the workflow file to run' }];
static override flags = {
...EASNonInteractiveFlag,
wait: Flags.boolean({
default: false,
allowNo: true,
description: 'Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.',
summary: 'Wait for workflow run to complete',
}),
...EasJsonOnlyFlag,
};
static override contextDefinition = {
@@ -32,6 +58,10 @@ export default class WorkflowRun extends EasCommand {
async runAsync(): Promise<void> {
const { flags, args } = await this.parse(WorkflowRun);
if (flags.json) {
enableJsonOutput();
}
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
@@ -80,8 +110,15 @@ export default class WorkflowRun extends EasCommand {
}
let projectArchiveBucketKey: string;
let easJsonBucketKey: string;
let packageJsonBucketKey: string;
let easJsonBucketKey: string | null = null;
let packageJsonBucketKey: string | null = null;
const easJsonPath = path.join(projectDir, 'eas.json');
const packageJsonPath = path.join(projectDir, 'package.json');
const projectRootDirectory = slash(
path.relative(await vcsClient.getRootPathAsync(), projectDir) || '.'
);
try {
({ projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
@@ -89,54 +126,165 @@ export default class WorkflowRun extends EasCommand {
vcsClient,
accountId: account.id,
}));
({ fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
graphqlClient,
accountId: account.id,
filePath: path.join(projectDir, 'eas.json'),
maxSizeBytes: 1024 * 1024,
}));
({ fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
graphqlClient,
accountId: account.id,
filePath: path.join(projectDir, 'package.json'),
maxSizeBytes: 1024 * 1024,
}));
if (await fileExistsAsync(easJsonPath)) {
({ fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
graphqlClient,
accountId: account.id,
filePath: easJsonPath,
maxSizeBytes: 1024 * 1024,
}));
} else {
Log.warn(
`⚠ No ${chalk.bold('eas.json')} found in the project directory. Running ${chalk.bold(
'type: build'
)} jobs will not work. Run ${chalk.bold(
'eas build:configure'
)} to configure your project for builds.`
);
}
if (await fileExistsAsync(packageJsonPath)) {
({ fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
graphqlClient,
accountId: account.id,
filePath: packageJsonPath,
maxSizeBytes: 1024 * 1024,
}));
} else {
Log.warn(
`⚠ No ${chalk.bold(
'package.json'
)} found in the project directory. It is used to automatically infer best job configuration for your project. You may want to define ${chalk.bold(
'image'
)} property in your workflow to specify the image to use.`
);
}
} catch (err) {
Log.error('Failed to upload project sources.');
throw err;
}
let workflowRunId: string;
try {
const { id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(
graphqlClient,
{
appId: projectId,
workflowRevisionInput: {
fileName: path.basename(args.file),
yamlConfig,
({ id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(graphqlClient, {
appId: projectId,
workflowRevisionInput: {
fileName: path.basename(args.file),
yamlConfig,
},
workflowRunInput: {
projectSource: {
type: WorkflowProjectSourceType.Gcs,
projectArchiveBucketKey,
easJsonBucketKey,
packageJsonBucketKey,
projectRootDirectory,
},
workflowRunInput: {
projectSource: {
type: WorkflowProjectSourceType.Gcs,
projectArchiveBucketKey,
easJsonBucketKey,
packageJsonBucketKey,
},
},
}
);
},
}));
Log.newLine();
Log.succeed(
`Workflow run started successfully. See logs: ${link(
getWorkflowRunUrl(account.name, projectName, workflowRunId)
)}`
);
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
} catch (err) {
Log.error('Failed to start the workflow with the API.');
throw err;
}
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput({
id: workflowRunId,
url: getWorkflowRunUrl(account.name, projectName, workflowRunId),
});
}
process.exit(0);
}
Log.newLine();
const { status } = await waitForWorkflowRunToEndAsync(graphqlClient, {
workflowRunId,
});
if (flags.json) {
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
printJsonOnlyOutput({
...workflowRun,
url: getWorkflowRunUrl(account.name, projectName, workflowRunId),
});
}
if (status === WorkflowRunStatus.Failure) {
process.exit(EXIT_CODES.WORKFLOW_FAILED);
} else if (status === WorkflowRunStatus.Canceled) {
process.exit(EXIT_CODES.WORKFLOW_CANCELED);
}
}
}
async function waitForWorkflowRunToEndAsync(
graphqlClient: ExpoGraphqlClient,
{ workflowRunId }: { workflowRunId: string }
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
const spinner = ora('Currently waiting for workflow run to start.').start();
let failedFetchesCount = 0;
while (true) {
try {
const workflowRun = await WorkflowRunQuery.byIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
failedFetchesCount = 0;
switch (workflowRun.status) {
case WorkflowRunStatus.InProgress:
spinner.start('Workflow run is in progress.');
break;
case WorkflowRunStatus.ActionRequired:
spinner.warn('Workflow run is waiting for action.');
break;
case WorkflowRunStatus.Canceled:
spinner.warn('Workflow run has been canceled.');
return workflowRun;
case WorkflowRunStatus.Failure:
spinner.fail('Workflow run has failed.');
return workflowRun;
case WorkflowRunStatus.Success:
spinner.succeed('Workflow run completed successfully.');
return workflowRun;
}
} catch {
spinner.text = '⚠ Failed to fetch the workflow run status. Check your network connection.';
failedFetchesCount += 1;
if (failedFetchesCount > 6) {
spinner.fail('Failed to fetch the workflow run status 6 times in a row. Aborting wait.');
process.exit(EXIT_CODES.WAIT_ABORTED);
}
}
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
}
}
async function fileExistsAsync(filePath: string): Promise<boolean> {
return await fs.promises
.access(filePath, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}
@@ -0,0 +1,95 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
import { getLimitFlagWithCustomValues } from '../../commandUtils/pagination';
import { processWorkflowRuns } from '../../commandUtils/workflows';
import { WorkflowRunFragment, WorkflowRunStatus } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log from '../../log';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class WorkflowRunList extends EasCommand {
static override description =
'list recent workflow runs for this project, with their IDs, statuses, and timestamps';
static override flags = {
workflow: Flags.string({
description:
'If present, the query will only return runs for the specified workflow file name',
required: false,
}),
status: Flags.enum({
description: 'If present, filter the returned runs to select those with the specified status',
required: false,
options: Object.values(WorkflowRunStatus),
}),
...EasJsonOnlyFlag,
limit: getLimitFlagWithCustomValues({ defaultTo: 10, limit: 100 }),
};
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(WorkflowRunList);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(WorkflowRunList, {
nonInteractive: true,
});
if (flags.json) {
enableJsonOutput();
}
const workflowFileName = flags.workflow;
const status = flags.status;
const limit = flags.limit ?? 10;
let runs: WorkflowRunFragment[];
if (workflowFileName) {
runs = await WorkflowRunQuery.byAppIdFileNameAndStatusAsync(
graphqlClient,
projectId,
workflowFileName,
status,
limit
);
} else {
runs = await AppQuery.byIdWorkflowRunsFilteredByStatusAsync(
graphqlClient,
projectId,
status,
limit
);
}
const result = processWorkflowRuns(runs);
if (flags.json) {
printJsonOnlyOutput(result);
return;
}
Log.addNewLineIfNone();
result.forEach(run => {
Log.log(
formatFields([
{ label: 'Run ID', value: run.id },
{ label: 'Workflow', value: run.workflowFileName },
{ label: 'Status', value: run.status },
{ label: 'Started At', value: run.startedAt },
{ label: 'Finished At', value: run.finishedAt },
{ label: 'Git Commit Message', value: run.gitCommitMessage ?? 'null' },
{ label: 'Git Commit Hash', value: run.gitCommitHash ?? 'null' },
])
);
Log.addNewLineIfNone();
});
}
}
@@ -14,9 +14,14 @@ import { displayAndroidAppCredentials } from '../printCredentials';
jest.mock('../../../../log');
jest.mock('chalk', () => ({
red: jest.fn(text => `red(${text})`),
blue: jest.fn(text => `blue(${text})`),
yellow: jest.fn(text => `yellow(${text})`),
green: jest.fn(text => `green(${text})`),
gray: jest.fn(text => `gray(${text})`),
bold: jest.fn(log => log),
cyan: { bold: jest.fn(log => log) },
dim: jest.fn(log => log),
cyan: { bold: jest.fn(log => log) },
}));
jest.mock('../../../../graphql/queries/AppQuery');
@@ -5,12 +5,29 @@ import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils
import { AppleDistributionCertificateQuery } from '../../api/graphql/queries/AppleDistributionCertificateQuery';
import { formatDistributionCertificate } from '../DistributionCertificateUtils';
jest.mock('../../api/graphql/queries/AppleDistributionCertificateQuery');
/*
jest.mock('chalk', () => {
return {
__esModule: true, // this property makes it work
default: { gray: jest.fn(log => log), underline: jest.fn(log => log) },
default: {
blue: jest.fn(log => log),
gray: jest.fn(log => log),
underline: jest.fn(log => log),
},
};
});
*/
jest.mock('chalk', () => ({
red: jest.fn(text => `red(${text})`),
blue: jest.fn(text => `blue(${text})`),
yellow: jest.fn(text => `yellow(${text})`),
green: jest.fn(text => `green(${text})`),
gray: jest.fn(log => log),
bold: jest.fn(log => log),
dim: jest.fn(log => log),
cyan: { bold: jest.fn(log => log) },
underline: jest.fn(log => log),
}));
mockdate.set(new Date('4/20/2021'));
describe('select credentials', () => {
@@ -9,7 +9,15 @@ import { App, Target } from '../../types';
import { displayIosCredentials } from '../printCredentials';
jest.mock('../../../../log');
jest.mock('chalk', () => ({ bold: jest.fn(log => log), cyan: { bold: jest.fn(log => log) } }));
jest.mock('chalk', () => ({
red: jest.fn(text => `red(${text})`),
blue: jest.fn(text => `blue(${text})`),
yellow: jest.fn(text => `yellow(${text})`),
green: jest.fn(text => `green(${text})`),
gray: jest.fn(text => `gray(${text})`),
bold: jest.fn(log => log),
cyan: { bold: jest.fn(log => log) },
}));
jest.mock('../../api/graphql/queries/IosAppCredentialsQuery');
mockdate.set(new Date('4/20/2021'));
+309 -122
View File
@@ -79,6 +79,8 @@ export type Account = {
__typename?: 'Account';
/** @deprecated Legacy access tokens are deprecated */
accessTokens: Array<Maybe<AccessToken>>;
/** Server account feature gate values for this account, optionally filtering by desired gates. */
accountFeatureGates: Scalars['JSONObject']['output'];
/** Coalesced project activity for all apps belonging to this account. */
activityTimelineProjectActivities: Array<ActivityTimelineProjectActivity>;
appCount: Scalars['Int']['output'];
@@ -116,9 +118,8 @@ export type Account = {
/** Billing information. Only visible to members with the ADMIN or OWNER role. */
billing?: Maybe<Billing>;
billingPeriod: BillingPeriod;
/** (EAS Build) Builds associated with this account */
builds: Array<Build>;
createdAt: Scalars['DateTime']['output'];
displayName?: Maybe<Scalars['String']['output']>;
/** Environment secrets for an account */
environmentSecrets: Array<EnvironmentSecret>;
/** Environment variables for an account */
@@ -144,7 +145,10 @@ export type Account = {
name: Scalars['String']['output'];
/** Offers set on this account */
offers?: Maybe<Array<Offer>>;
/** Owning User of this account if personal account */
/**
* Owning User of this account if personal account
* @deprecated Deprecated in favor of ownerUserActor
*/
owner?: Maybe<User>;
/** Owning UserActor of this account if personal account */
ownerUserActor?: Maybe<UserActor>;
@@ -185,6 +189,15 @@ export type Account = {
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
*/
export type AccountAccountFeatureGatesArgs = {
filter?: InputMaybe<Array<Scalars['String']['input']>>;
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
@@ -356,18 +369,6 @@ export type AccountBillingPeriodArgs = {
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
*/
export type AccountBuildsArgs = {
limit: Scalars['Int']['input'];
offset: Scalars['Int']['input'];
platform?: InputMaybe<AppPlatform>;
status?: InputMaybe<BuildStatus>;
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
@@ -557,12 +558,16 @@ export type AccountMutation = {
changePlan: Account;
/** Add specified account Permissions for Actor. Actor must already have at least one permission on the account. */
grantActorPermissions: Account;
/** Remove profile image for the account. Do nothing if there's no profile image associated. */
removeProfileImage: Account;
/** Rename this account and the primary user's username if this account is a personal account */
rename: Account;
/** Requests a refund for the specified charge by requesting a manual refund from support */
requestRefund?: Maybe<Scalars['Boolean']['output']>;
/** Revoke specified Permissions for Actor. Actor must already have at least one permission on the account. */
revokeActorPermissions: Account;
/** Set the display name for the account. */
setDisplayName: Account;
/** Require authorization to send push notifications for experiences owned by this account */
setPushSecurityEnabled: Account;
};
@@ -598,6 +603,11 @@ export type AccountMutationGrantActorPermissionsArgs = {
};
export type AccountMutationRemoveProfileImageArgs = {
accountID: Scalars['ID']['input'];
};
export type AccountMutationRenameArgs = {
accountID: Scalars['ID']['input'];
newName: Scalars['String']['input'];
@@ -619,6 +629,12 @@ export type AccountMutationRevokeActorPermissionsArgs = {
};
export type AccountMutationSetDisplayNameArgs = {
accountID: Scalars['ID']['input'];
displayName: Scalars['String']['input'];
};
export type AccountMutationSetPushSecurityEnabledArgs = {
accountID: Scalars['ID']['input'];
pushSecurityEnabled: Scalars['Boolean']['input'];
@@ -1213,6 +1229,7 @@ export type AndroidSubmissionConfig = {
export type AndroidSubmissionConfigInput = {
applicationIdentifier?: InputMaybe<Scalars['String']['input']>;
archiveUrl?: InputMaybe<Scalars['String']['input']>;
changelog?: InputMaybe<Scalars['String']['input']>;
changesNotSentForReview?: InputMaybe<Scalars['Boolean']['input']>;
googleServiceAccountKeyId?: InputMaybe<Scalars['String']['input']>;
googleServiceAccountKeyJson?: InputMaybe<Scalars['String']['input']>;
@@ -1395,6 +1412,7 @@ export type App = Project & {
workerDeploymentsCrashes?: Maybe<WorkerDeploymentCrashes>;
workerDeploymentsRequest: WorkerDeploymentRequestEdge;
workerDeploymentsRequests?: Maybe<WorkerDeploymentRequests>;
workflowRunGitBranchesPaginated: AppWorkflowRunGitBranchesConnection;
workflowRunsPaginated: AppWorkflowRunsConnection;
workflows: Array<Workflow>;
};
@@ -1681,10 +1699,21 @@ export type AppWorkerDeploymentsRequestsArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppWorkflowRunGitBranchesPaginatedArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<WorkflowRunGitBranchFilterInput>;
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppWorkflowRunsPaginatedArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<AppWorkflowRunFilterInput>;
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
};
@@ -1821,10 +1850,10 @@ export type AppMutation = {
__typename?: 'AppMutation';
/** Create an app */
createApp: App;
/** Create an app and GitHub repository if user desire to */
createAppAndGithubRepository: CreateAppAndGithubRepositoryResponse;
/** @deprecated No longer supported */
grantAccess?: Maybe<App>;
/** Remove profile image (icon) for the app. Do nothing if there's no profile image associated. */
removeProfileImage: App;
/** Delete an App. Returns the ID of the background job receipt. Use BackgroundJobReceiptQuery to get the status of the job. */
scheduleAppDeletion: BackgroundJobReceipt;
/** Set display info for app */
@@ -1841,17 +1870,17 @@ export type AppMutationCreateAppArgs = {
};
export type AppMutationCreateAppAndGithubRepositoryArgs = {
appInput: AppWithGithubRepositoryInput;
};
export type AppMutationGrantAccessArgs = {
accessLevel?: InputMaybe<Scalars['String']['input']>;
toUser: Scalars['ID']['input'];
};
export type AppMutationRemoveProfileImageArgs = {
appId: Scalars['ID']['input'];
};
export type AppMutationScheduleAppDeletionArgs = {
appId: Scalars['ID']['input'];
};
@@ -2188,6 +2217,29 @@ export type AppWorkflowRunEdge = {
node: WorkflowRun;
};
export type AppWorkflowRunFilterInput = {
requestedGitRef?: InputMaybe<Scalars['String']['input']>;
status?: InputMaybe<WorkflowRunStatus>;
};
export type AppWorkflowRunGitBranchEdge = {
__typename?: 'AppWorkflowRunGitBranchEdge';
cursor: Scalars['String']['output'];
node: AppWorkflowRunGitBranchNode;
};
export type AppWorkflowRunGitBranchNode = {
__typename?: 'AppWorkflowRunGitBranchNode';
lastRunAt: Scalars['DateTime']['output'];
name: Scalars['String']['output'];
};
export type AppWorkflowRunGitBranchesConnection = {
__typename?: 'AppWorkflowRunGitBranchesConnection';
edges: Array<AppWorkflowRunGitBranchEdge>;
pageInfo: PageInfo;
};
export type AppWorkflowRunsConnection = {
__typename?: 'AppWorkflowRunsConnection';
edges: Array<AppWorkflowRunEdge>;
@@ -2362,6 +2414,16 @@ export type AppleDistributionCertificateMutationDeleteAppleDistributionCertifica
id: Scalars['ID']['input'];
};
export type AppleDistributionCertificateQuery = {
__typename?: 'AppleDistributionCertificateQuery';
byId: AppleDistributionCertificate;
};
export type AppleDistributionCertificateQueryByIdArgs = {
id: Scalars['ID']['input'];
};
export type AppleProvisioningProfile = {
__typename?: 'AppleProvisioningProfile';
account: Account;
@@ -2418,6 +2480,16 @@ export type AppleProvisioningProfileMutationUpdateAppleProvisioningProfileArgs =
id: Scalars['ID']['input'];
};
export type AppleProvisioningProfileQuery = {
__typename?: 'AppleProvisioningProfileQuery';
byId: AppleProvisioningProfile;
};
export type AppleProvisioningProfileQueryByIdArgs = {
id: Scalars['ID']['input'];
};
export type ApplePushKey = {
__typename?: 'ApplePushKey';
account: Account;
@@ -2550,6 +2622,16 @@ export type AscApiKeyInput = {
keyP8: Scalars['String']['input'];
};
export type Asset = {
__typename?: 'Asset';
contentType: Scalars['String']['output'];
fileSHA256: Scalars['String']['output'];
fileSize: Scalars['Int']['output'];
finalFileSize?: Maybe<Scalars['Int']['output']>;
id: Scalars['ID']['output'];
storageKey: Scalars['String']['output'];
};
export type AssetMapGroup = {
android?: InputMaybe<AssetMapSourceInput>;
ios?: InputMaybe<AssetMapSourceInput>;
@@ -2593,7 +2675,15 @@ export type AssetMutationGetSignedAssetUploadSpecificationsArgs = {
/** Check to see if assets with given storageKeys exist */
export type AssetQuery = {
__typename?: 'AssetQuery';
byStorageKeys: Array<Asset>;
metadata: Array<AssetMetadataResult>;
signedUrls: Array<AssetSignedUrlResult>;
};
/** Check to see if assets with given storageKeys exist */
export type AssetQueryByStorageKeysArgs = {
storageKeys: Array<Scalars['String']['input']>;
};
@@ -2602,6 +2692,20 @@ export type AssetQueryMetadataArgs = {
storageKeys: Array<Scalars['String']['input']>;
};
/** Check to see if assets with given storageKeys exist */
export type AssetQuerySignedUrlsArgs = {
storageKeys: Array<Scalars['String']['input']>;
updateId: Scalars['ID']['input'];
};
export type AssetSignedUrlResult = {
__typename?: 'AssetSignedUrlResult';
headers?: Maybe<Scalars['JSON']['output']>;
storageKey: Scalars['String']['output'];
url: Scalars['String']['output'];
};
export type AuditLog = {
__typename?: 'AuditLog';
account?: Maybe<Account>;
@@ -2676,6 +2780,7 @@ export enum AuthProtocolType {
}
export enum AuthProviderIdentifier {
Generic = 'GENERIC',
GoogleWs = 'GOOGLE_WS',
MsEntraId = 'MS_ENTRA_ID',
Okta = 'OKTA',
@@ -2683,9 +2788,11 @@ export enum AuthProviderIdentifier {
StubIdp = 'STUB_IDP'
}
export type AverageMetrics = {
__typename?: 'AverageMetrics';
averageDownloadSize: Scalars['Int']['output'];
export type AverageAssetMetrics = {
__typename?: 'AverageAssetMetrics';
averageDownloadSizeBytes: Scalars['Int']['output'];
count: Scalars['Int']['output'];
storageKey: Scalars['String']['output'];
};
export type BackgroundJobReceipt = {
@@ -2801,8 +2908,6 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
/** Queue position is 1-indexed */
initialQueuePosition?: Maybe<Scalars['Int']['output']>;
initiatingActor?: Maybe<Actor>;
/** @deprecated User type is deprecated */
initiatingUser?: Maybe<User>;
iosEnterpriseProvisioning?: Maybe<BuildIosEnterpriseProvisioning>;
isForIosSimulator: Scalars['Boolean']['output'];
isGitWorkingTreeDirty?: Maybe<Scalars['Boolean']['output']>;
@@ -2871,6 +2976,7 @@ export type BuildAnnotation = {
regexFlags?: Maybe<Scalars['String']['output']>;
regexString: Scalars['String']['output'];
title: Scalars['String']['output'];
type: BuildAnnotationType;
};
export type BuildAnnotationDataInput = {
@@ -2881,6 +2987,7 @@ export type BuildAnnotationDataInput = {
regexFlags?: InputMaybe<Scalars['String']['input']>;
regexString: Scalars['String']['input'];
title: Scalars['String']['input'];
type: BuildAnnotationType;
};
export type BuildAnnotationFiltersInput = {
@@ -2913,6 +3020,12 @@ export type BuildAnnotationMutationUpdateBuildAnnotationArgs = {
buildAnnotationId: Scalars['ID']['input'];
};
export enum BuildAnnotationType {
Error = 'ERROR',
Info = 'INFO',
Warning = 'WARNING'
}
export type BuildAnnotationsQuery = {
__typename?: 'BuildAnnotationsQuery';
/** View build annotations */
@@ -3437,12 +3550,6 @@ export type CreateAndroidSubmissionInput = {
submittedBuildId?: InputMaybe<Scalars['ID']['input']>;
};
export type CreateAppAndGithubRepositoryResponse = {
__typename?: 'CreateAppAndGithubRepositoryResponse';
app: App;
cloneUrl?: Maybe<Scalars['String']['output']>;
};
export type CreateBuildResult = {
__typename?: 'CreateBuildResult';
build: Build;
@@ -3544,6 +3651,12 @@ export type CreateSubmissionResult = {
submission: Submission;
};
export type CumulativeAverageMetrics = {
__typename?: 'CumulativeAverageMetrics';
averageUpdatePayloadBytes: Scalars['Int']['output'];
launchAssetCount: Scalars['Int']['output'];
};
export type CumulativeMetrics = {
__typename?: 'CumulativeMetrics';
data: UpdatesMetricsData;
@@ -4007,6 +4120,7 @@ export enum EasServiceMetric {
AssetsRequests = 'ASSETS_REQUESTS',
BandwidthUsage = 'BANDWIDTH_USAGE',
Builds = 'BUILDS',
LocalBuilds = 'LOCAL_BUILDS',
ManifestRequests = 'MANIFEST_REQUESTS',
RunTime = 'RUN_TIME',
UniqueUpdaters = 'UNIQUE_UPDATERS',
@@ -4115,6 +4229,7 @@ export enum EnvironmentSecretType {
export type EnvironmentVariable = {
__typename?: 'EnvironmentVariable';
/** @deprecated Environment variables are automatically linked to all apps */
apps: Array<App>;
createdAt: Scalars['DateTime']['output'];
environments?: Maybe<Array<EnvironmentVariableEnvironment>>;
@@ -4161,12 +4276,6 @@ export type EnvironmentVariableMutation = {
deleteBulkEnvironmentVariables: Array<DeleteEnvironmentVariableResult>;
/** Delete an environment variable */
deleteEnvironmentVariable: DeleteEnvironmentVariableResult;
/** Bulk link shared environment variables */
linkBulkSharedEnvironmentVariables: Array<EnvironmentVariable>;
/** Link shared environment variable */
linkSharedEnvironmentVariable: EnvironmentVariable;
/** Unlink shared environment variable */
unlinkSharedEnvironmentVariable: EnvironmentVariable;
/** Bulk update environment variables */
updateBulkEnvironmentVariables: Array<EnvironmentVariable>;
/** Update an environment variable */
@@ -4208,25 +4317,6 @@ export type EnvironmentVariableMutationDeleteEnvironmentVariableArgs = {
};
export type EnvironmentVariableMutationLinkBulkSharedEnvironmentVariablesArgs = {
linkData: Array<LinkSharedEnvironmentVariableInput>;
};
export type EnvironmentVariableMutationLinkSharedEnvironmentVariableArgs = {
appId: Scalars['ID']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
};
export type EnvironmentVariableMutationUnlinkSharedEnvironmentVariableArgs = {
appId: Scalars['ID']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
};
export type EnvironmentVariableMutationUpdateBulkEnvironmentVariablesArgs = {
environmentVariablesData: Array<UpdateEnvironmentVariableInput>;
};
@@ -4344,12 +4434,14 @@ export enum Feature {
export type Fingerprint = {
__typename?: 'Fingerprint';
app: App;
buildCount: Scalars['Int']['output'];
builds: AppBuildsConnection;
createdAt: Scalars['DateTime']['output'];
debugInfoUrl?: Maybe<Scalars['String']['output']>;
hash: Scalars['String']['output'];
id: Scalars['ID']['output'];
source?: Maybe<FingerprintSource>;
updateCount: Scalars['Int']['output'];
updatedAt: Scalars['DateTime']['output'];
updates: AppUpdatesConnection;
};
@@ -4561,8 +4653,6 @@ export type GitHubBuildInput = {
environment?: InputMaybe<EnvironmentVariableEnvironment>;
gitRef: Scalars['String']['input'];
platform: AppPlatform;
/** Repack the golden dev client build instead of running full build process. Used for onboarding. Do not use outside of onboarding flow, as for now it's only created with this specific use case in mind. */
repack?: InputMaybe<Scalars['Boolean']['input']>;
submitProfile?: InputMaybe<Scalars['String']['input']>;
};
@@ -5237,6 +5327,7 @@ export type IosSubmissionConfigInput = {
ascApiKey?: InputMaybe<AscApiKeyInput>;
ascApiKeyId?: InputMaybe<Scalars['String']['input']>;
ascAppIdentifier: Scalars['String']['input'];
changelog?: InputMaybe<Scalars['String']['input']>;
groups?: InputMaybe<Array<Scalars['String']['input']>>;
isVerboseFastlaneEnabled?: InputMaybe<Scalars['Boolean']['input']>;
};
@@ -5371,12 +5462,6 @@ export type LinkSentryInstallationToExpoAccountInput = {
sentryOrgSlug: Scalars['String']['input'];
};
export type LinkSharedEnvironmentVariableInput = {
appId: Scalars['ID']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
};
export type LocalBuildArchiveSourceInput = {
bucketKey: Scalars['String']['input'];
type: LocalBuildArchiveSourceType;
@@ -5868,6 +5953,7 @@ export type PublicArtifacts = {
};
export type PublishUpdateGroupInput = {
assetHostOverride?: InputMaybe<Scalars['String']['input']>;
assetMapGroup?: InputMaybe<AssetMapGroup>;
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']['input']>;
branchId: Scalars['String']['input'];
@@ -5876,6 +5962,7 @@ export type PublishUpdateGroupInput = {
fingerprintInfoGroup?: InputMaybe<FingerprintInfoGroup>;
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
isGitWorkingTreeDirty?: InputMaybe<Scalars['Boolean']['input']>;
manifestHostOverride?: InputMaybe<Scalars['String']['input']>;
message?: InputMaybe<Scalars['String']['input']>;
rollBackToEmbeddedInfoGroup?: InputMaybe<UpdateRollBackToEmbeddedGroup>;
rolloutInfoGroup?: InputMaybe<UpdateRolloutInfoGroup>;
@@ -5897,6 +5984,7 @@ export enum RequestMethod {
export type RequestsFilters = {
cacheStatus?: InputMaybe<Array<ResponseCacheStatus>>;
continent?: InputMaybe<Array<ContinentCode>>;
country?: InputMaybe<Array<Scalars['String']['input']>>;
hasCustomDomainOrigin?: InputMaybe<Scalars['Boolean']['input']>;
isAsset?: InputMaybe<Scalars['Boolean']['input']>;
isCrash?: InputMaybe<Scalars['Boolean']['input']>;
@@ -6149,7 +6237,7 @@ export type RootMutation = {
webhook: WebhookMutation;
/** Mutations that modify a websiteNotification */
websiteNotifications: WebsiteNotificationMutation;
workflowJob: WorkflowJobMutation;
workflowJobApproval: WorkflowJobApprovalMutation;
workflowRevision: WorkflowRevisionMutation;
workflowRun: WorkflowRunMutation;
};
@@ -6200,6 +6288,10 @@ export type RootQuery = {
appStoreConnectApiKey: AppStoreConnectApiKeyQuery;
/** Top-level query object for querying Apple Device registration requests. */
appleDeviceRegistrationRequest: AppleDeviceRegistrationRequestQuery;
/** Top-level query object for querying Apple distribution certificates. */
appleDistributionCertificate?: Maybe<AppleDistributionCertificateQuery>;
/** Top-level query object for querying Apple provisioning profiles. */
appleProvisioningProfile?: Maybe<AppleProvisioningProfileQuery>;
/** Top-level query object for querying Apple Teams. */
appleTeam: AppleTeamQuery;
asset: AssetQuery;
@@ -6303,6 +6395,7 @@ export type RootQueryAppByAppIdArgs = {
export type RootQueryUpdatesByGroupArgs = {
group: Scalars['ID']['input'];
platform?: InputMaybe<Scalars['String']['input']>;
};
@@ -6735,6 +6828,7 @@ export enum StatuspageServiceName {
EasBuild = 'EAS_BUILD',
EasSubmit = 'EAS_SUBMIT',
EasUpdate = 'EAS_UPDATE',
EasWorkflows = 'EAS_WORKFLOWS',
GithubApiRequests = 'GITHUB_API_REQUESTS',
GithubWebhooks = 'GITHUB_WEBHOOKS'
}
@@ -6985,6 +7079,7 @@ export type Update = ActivityTimelineProjectActivity & {
activityTimestamp: Scalars['DateTime']['output'];
actor?: Maybe<Actor>;
app: App;
assetHostOverride?: Maybe<Scalars['String']['output']>;
assetMapUrl?: Maybe<Scalars['String']['output']>;
awaitingCodeSigningInfo: Scalars['Boolean']['output'];
branch: UpdateBranch;
@@ -7004,6 +7099,7 @@ export type Update = ActivityTimelineProjectActivity & {
isRollBackToEmbedded: Scalars['Boolean']['output'];
jobRun?: Maybe<JobRun>;
manifestFragment: Scalars['String']['output'];
manifestHostOverride?: Maybe<Scalars['String']['output']>;
manifestPermalink: Scalars['String']['output'];
message?: Maybe<Scalars['String']['output']>;
platform: Scalars['String']['output'];
@@ -7227,7 +7323,8 @@ export type UpdateInfoGroup = {
export type UpdateInsights = {
__typename?: 'UpdateInsights';
averageMetrics: AverageMetrics;
averageAssetMetrics: Array<AverageAssetMetrics>;
cumulativeAverageMetrics: CumulativeAverageMetrics;
cumulativeMetrics: CumulativeMetrics;
id: Scalars['ID']['output'];
totalUniqueUsers: Scalars['Int']['output'];
@@ -7298,7 +7395,7 @@ export type UpdateRollBackToEmbeddedGroup = {
};
export type UpdateRolloutInfo = {
rolloutControlUpdateId: Scalars['ID']['input'];
rolloutControlUpdateId?: InputMaybe<Scalars['ID']['input']>;
rolloutPercentage: Scalars['Int']['input'];
};
@@ -7814,7 +7911,12 @@ export enum UserEntityTypeName {
export type UserInvitation = {
__typename?: 'UserInvitation';
accountName: Scalars['String']['output'];
/** If the invite is for a personal team, the profile photo of account owner */
/** The profile image URL of the account owner */
accountProfileImageUrl: Scalars['String']['output'];
/**
* If the invite is for a personal team, the profile photo of account owner
* @deprecated Use accountProfileImageUrl
*/
accountProfilePhoto?: Maybe<Scalars['String']['output']>;
created: Scalars['DateTime']['output'];
/** Email to which this invitation was sent */
@@ -7891,6 +7993,7 @@ export type UserInvitationMutationResendUserInvitationArgs = {
export type UserInvitationPublicData = {
__typename?: 'UserInvitationPublicData';
accountName: Scalars['String']['output'];
accountProfileImageUrl: Scalars['String']['output'];
accountProfilePhoto?: Maybe<Scalars['String']['output']>;
created: Scalars['DateTime']['output'];
email: Scalars['String']['output'];
@@ -8126,6 +8229,8 @@ export type WorkerDeployment = ActivityTimelineProjectActivity & {
initiatingActor?: Maybe<Actor>;
logs?: Maybe<WorkerDeploymentLogs>;
requests?: Maybe<WorkerDeploymentRequests>;
signedAssetsURL: Scalars['String']['output'];
signedDeploymentURL: Scalars['String']['output'];
subdomain: Scalars['String']['output'];
url: Scalars['String']['output'];
};
@@ -8318,6 +8423,7 @@ export type WorkerDeploymentRequestNode = {
requestId: Scalars['WorkerDeploymentRequestID']['output'];
requestTimestamp: Scalars['DateTime']['output'];
responseType: ResponseType;
routerPath?: Maybe<Scalars['String']['output']>;
scriptName: Scalars['String']['output'];
search?: Maybe<Scalars['String']['output']>;
status: Scalars['Int']['output'];
@@ -8561,6 +8667,7 @@ export type WorkflowRevisionsPaginatedArgs = {
export type WorkflowRunsPaginatedArgs = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<WorkflowRunFilterInput>;
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
};
@@ -8575,11 +8682,18 @@ export type WorkflowArtifact = {
id: Scalars['ID']['output'];
jobRun: JobRun;
name: Scalars['String']['output'];
storageType: WorkflowArtifactStorageType;
updatedAt: Scalars['DateTime']['output'];
};
export enum WorkflowArtifactStorageType {
Gcs = 'GCS',
R2 = 'R2'
}
export type WorkflowJob = {
__typename?: 'WorkflowJob';
approvals: Array<WorkflowJobApproval>;
createdAt: Scalars['DateTime']['output'];
credentialsAppleDeviceRegistrationRequest?: Maybe<AppleDeviceRegistrationRequest>;
errors: Array<WorkflowJobError>;
@@ -8597,22 +8711,33 @@ export type WorkflowJob = {
workflowRun: WorkflowRun;
};
export type WorkflowJobApproval = {
__typename?: 'WorkflowJobApproval';
createdAt: Scalars['DateTime']['output'];
decision: WorkflowJobReviewDecision;
id: Scalars['ID']['output'];
updatedAt: Scalars['DateTime']['output'];
userActor?: Maybe<UserActor>;
workflowJob: WorkflowJob;
};
export type WorkflowJobApprovalMutation = {
__typename?: 'WorkflowJobApprovalMutation';
setWorkflowJobApprovalDecision: WorkflowJobApproval;
};
export type WorkflowJobApprovalMutationSetWorkflowJobApprovalDecisionArgs = {
decision: WorkflowJobReviewDecision;
workflowJobId: Scalars['ID']['input'];
};
export type WorkflowJobError = {
__typename?: 'WorkflowJobError';
message: Scalars['String']['output'];
title: Scalars['String']['output'];
};
export type WorkflowJobMutation = {
__typename?: 'WorkflowJobMutation';
approveWorkflowJob: Scalars['ID']['output'];
};
export type WorkflowJobMutationApproveWorkflowJobArgs = {
workflowJobId: Scalars['ID']['input'];
};
export type WorkflowJobQuery = {
__typename?: 'WorkflowJobQuery';
byId: WorkflowJob;
@@ -8623,6 +8748,11 @@ export type WorkflowJobQueryByIdArgs = {
workflowJobId: Scalars['ID']['input'];
};
export enum WorkflowJobReviewDecision {
Approved = 'APPROVED',
Rejected = 'REJECTED'
}
export enum WorkflowJobStatus {
ActionRequired = 'ACTION_REQUIRED',
Canceled = 'CANCELED',
@@ -8639,9 +8769,12 @@ export enum WorkflowJobType {
Build = 'BUILD',
Custom = 'CUSTOM',
Deploy = 'DEPLOY',
Doc = 'DOC',
Fingerprint = 'FINGERPRINT',
GetBuild = 'GET_BUILD',
MaestroCloud = 'MAESTRO_CLOUD',
MaestroTest = 'MAESTRO_TEST',
Repack = 'REPACK',
RequireApproval = 'REQUIRE_APPROVAL',
Slack = 'SLACK',
Submission = 'SUBMISSION',
@@ -8649,9 +8782,10 @@ export enum WorkflowJobType {
}
export type WorkflowProjectSourceInput = {
easJsonBucketKey: Scalars['String']['input'];
easJsonBucketKey?: InputMaybe<Scalars['String']['input']>;
packageJsonBucketKey?: InputMaybe<Scalars['String']['input']>;
projectArchiveBucketKey: Scalars['String']['input'];
projectRootDirectory?: InputMaybe<Scalars['String']['input']>;
type: WorkflowProjectSourceType;
};
@@ -8659,14 +8793,21 @@ export enum WorkflowProjectSourceType {
Gcs = 'GCS'
}
/** Look up Workflow by ID */
export type WorkflowQuery = {
__typename?: 'WorkflowQuery';
/** Look up Workflow by app ID and file name */
byAppIdAndFileName: Workflow;
/** Look up Workflow by ID */
byId: Workflow;
};
/** Look up Workflow by ID */
export type WorkflowQueryByAppIdAndFileNameArgs = {
appId: Scalars['ID']['input'];
fileName: Scalars['String']['input'];
};
export type WorkflowQueryByIdArgs = {
workflowId: Scalars['ID']['input'];
};
@@ -8724,12 +8865,12 @@ export type WorkflowRun = ActivityTimelineProjectActivity & {
activityTimestamp: Scalars['DateTime']['output'];
actor?: Maybe<Actor>;
createdAt: Scalars['DateTime']['output'];
durationSeconds?: Maybe<Scalars['Int']['output']>;
errors: Array<WorkflowRunError>;
gitCommitHash?: Maybe<Scalars['String']['output']>;
gitCommitMessage?: Maybe<Scalars['String']['output']>;
githubRepository?: Maybe<GitHubRepository>;
id: Scalars['ID']['output'];
initiatingUser?: Maybe<User>;
jobs: Array<WorkflowJob>;
name: Scalars['String']['output'];
pullRequestNumber?: Maybe<Scalars['Int']['output']>;
@@ -8758,6 +8899,15 @@ export type WorkflowRunError = {
title?: Maybe<Scalars['String']['output']>;
};
export type WorkflowRunFilterInput = {
requestedGitRef?: InputMaybe<Scalars['String']['input']>;
status?: InputMaybe<WorkflowRunStatus>;
};
export type WorkflowRunGitBranchFilterInput = {
searchTerm?: InputMaybe<Scalars['String']['input']>;
};
export type WorkflowRunInput = {
projectSource: WorkflowProjectSourceInput;
};
@@ -8803,7 +8953,6 @@ export enum WorkflowRunStatus {
Failure = 'FAILURE',
InProgress = 'IN_PROGRESS',
New = 'NEW',
PendingCancel = 'PENDING_CANCEL',
Success = 'SUCCESS'
}
@@ -8932,13 +9081,6 @@ export type AppInfoQueryVariables = Exact<{
export type AppInfoQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, fullName: string } } };
export type ScheduleUpdateGroupDeletionMutationVariables = Exact<{
group: Scalars['ID']['input'];
}>;
export type ScheduleUpdateGroupDeletionMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', scheduleUpdateGroupDeletion: { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any } } };
export type CreateAndroidAppBuildCredentialsMutationVariables = Exact<{
androidAppBuildCredentialsInput: AndroidAppBuildCredentialsInput;
androidAppCredentialsId: Scalars['ID']['input'];
@@ -9423,24 +9565,6 @@ export type DeleteEnvironmentSecretMutationVariables = Exact<{
export type DeleteEnvironmentSecretMutation = { __typename?: 'RootMutation', environmentSecret: { __typename?: 'EnvironmentSecretMutation', deleteEnvironmentSecret: { __typename?: 'DeleteEnvironmentSecretResult', id: string } } };
export type LinkSharedEnvironmentVariableMutationVariables = Exact<{
appId: Scalars['ID']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
}>;
export type LinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', linkSharedEnvironmentVariable: { __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 UnlinkSharedEnvironmentVariableMutationVariables = Exact<{
appId: Scalars['ID']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
}>;
export type UnlinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', unlinkSharedEnvironmentVariable: { __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 CreateEnvironmentVariableForAccountMutationVariables = Exact<{
input: CreateSharedEnvironmentVariableInput;
accountId: Scalars['ID']['input'];
@@ -9514,7 +9638,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> } };
export type SetCodeSigningInfoMutationVariables = Exact<{
updateId: Scalars['ID']['input'];
@@ -9530,7 +9654,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
export type CreateAndroidSubmissionMutationVariables = Exact<{
appId: Scalars['ID']['input'];
@@ -9615,6 +9739,13 @@ export type CreateWorkflowRunMutationVariables = Exact<{
export type CreateWorkflowRunMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', createWorkflowRun: { __typename?: 'WorkflowRun', id: string } } };
export type CancelWorkflowRunMutationVariables = Exact<{
workflowRunId: Scalars['ID']['input'];
}>;
export type CancelWorkflowRunMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', cancelWorkflowRun: { __typename?: 'WorkflowRun', id: string } } };
export type AppByIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
}>;
@@ -9629,6 +9760,22 @@ export type AppByFullNameQueryVariables = Exact<{
export type AppByFullNameQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byFullName: { __typename?: 'App', id: string, name: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, githubRepository?: { __typename?: 'GitHubRepository', id: string, metadata: { __typename?: 'GitHubRepositoryMetadata', githubRepoOwnerName: string, githubRepoName: string } } | null } } };
export type AppByIdWorkflowsQueryVariables = Exact<{
appId: Scalars['String']['input'];
}>;
export type AppByIdWorkflowsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, workflows: Array<{ __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any }> } } };
export type AppByIdWorkflowRunsFilteredByStatusQueryVariables = Exact<{
appId: Scalars['String']['input'];
status?: InputMaybe<WorkflowRunStatus>;
limit: Scalars['Int']['input'];
}>;
export type AppByIdWorkflowRunsFilteredByStatusQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, runs: { __typename?: 'AppWorkflowRunsConnection', edges: Array<{ __typename?: 'AppWorkflowRunEdge', node: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string } } }> } } } };
export type AppStoreConnectApiKeyByIdQueryVariables = Exact<{
ascApiKeyId: Scalars['ID']['input'];
}>;
@@ -9677,7 +9824,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> }> } } };
export type BranchesBasicPaginatedOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9698,7 +9845,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
export type BuildsByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
@@ -9746,7 +9893,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9755,7 +9902,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> }> } } };
export type ViewUpdateChannelsPaginatedOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9891,7 +10038,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9902,7 +10049,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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } | null } } };
export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9912,14 +10059,14 @@ 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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } } };
export type UpdateByIdQueryVariables = Exact<{
updateId: Scalars['ID']['input'];
}>;
export type UpdateByIdQuery = { __typename?: 'RootQuery', updates: { __typename?: 'UpdateQuery', byId: { __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, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
export type UpdateByIdQuery = { __typename?: 'RootQuery', updates: { __typename?: 'UpdateQuery', byId: { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
@@ -9941,6 +10088,30 @@ export type WebhookByIdQueryVariables = Exact<{
export type WebhookByIdQuery = { __typename?: 'RootQuery', webhook: { __typename?: 'WebhookQuery', byId: { __typename?: 'Webhook', id: string, event: WebhookType, url: string, createdAt: any, updatedAt: any } } };
export type WorkflowRunByIdQueryVariables = Exact<{
workflowRunId: Scalars['ID']['input'];
}>;
export type WorkflowRunByIdQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus } } };
export type WorkflowRunByIdWithJobsQueryVariables = Exact<{
workflowRunId: Scalars['ID']['input'];
}>;
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, name: string, status: WorkflowRunStatus, createdAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, type: WorkflowJobType, status: WorkflowJobStatus, outputs: any, createdAt: any }> } } };
export type WorkflowRunsForAppIdFileNameAndStatusQueryVariables = Exact<{
appId: Scalars['ID']['input'];
fileName: Scalars['String']['input'];
status?: InputMaybe<WorkflowRunStatus>;
limit: Scalars['Int']['input'];
}>;
export type WorkflowRunsForAppIdFileNameAndStatusQuery = { __typename?: 'RootQuery', workflows: { __typename?: 'WorkflowQuery', byAppIdAndFileName: { __typename?: 'Workflow', id: string, runs: { __typename?: 'WorkflowRunsConnection', edges: Array<{ __typename?: 'WorkflowRunEdge', node: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string } } }> } } } };
export type AccountFragment = { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> };
export type AppFragment = { __typename?: 'App', id: string, name: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, githubRepository?: { __typename?: 'GitHubRepository', id: string, metadata: { __typename?: 'GitHubRepositoryMetadata', githubRepoOwnerName: string, githubRepoName: string } } | null };
@@ -9967,9 +10138,9 @@ export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id:
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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, 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, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, 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, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | 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, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
export type UpdateBranchBasicInfoFragment = { __typename?: 'UpdateBranch', id: string, name: string };
@@ -9977,6 +10148,10 @@ export type UpdateChannelBasicInfoFragment = { __typename?: 'UpdateChannel', id:
export type WebhookFragment = { __typename?: 'Webhook', id: string, event: WebhookType, url: string, createdAt: any, updatedAt: any };
export type WorkflowFragment = { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any };
export type WorkflowRunFragment = { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string } };
export type AndroidAppBuildCredentialsFragment = { __typename?: 'AndroidAppBuildCredentials', id: string, isDefault: boolean, isLegacy: boolean, name: string, androidKeystore?: { __typename?: 'AndroidKeystore', id: string, type: AndroidKeystoreType, keystore: string, keystorePassword: string, keyAlias: string, keyPassword?: string | null, md5CertificateFingerprint?: string | null, sha1CertificateFingerprint?: string | null, sha256CertificateFingerprint?: string | null, createdAt: any, updatedAt: any } | null };
export type CommonAndroidAppCredentialsFragment = { __typename?: 'AndroidAppCredentials', id: string, applicationIdentifier?: string | null, isLegacy: boolean, app: { __typename?: 'App', id: string, name: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, githubRepository?: { __typename?: 'GitHubRepository', id: string, metadata: { __typename?: 'GitHubRepositoryMetadata', githubRepoOwnerName: string, githubRepoName: string } } | null }, androidFcm?: { __typename?: 'AndroidFcm', id: string, credential: any, version: AndroidFcmVersion, createdAt: any, updatedAt: any, snippet: { __typename?: 'FcmSnippetLegacy', firstFourCharacters: string, lastFourCharacters: string } | { __typename?: 'FcmSnippetV1', projectId: string, keyId: string, serviceAccountEmail: string, clientId?: string | null } } | null, googleServiceAccountKeyForFcmV1?: { __typename?: 'GoogleServiceAccountKey', id: string, projectIdentifier: string, privateKeyIdentifier: string, clientEmail: string, clientIdentifier: string, createdAt: any, updatedAt: any } | null, googleServiceAccountKeyForSubmissions?: { __typename?: 'GoogleServiceAccountKey', id: string, projectIdentifier: string, privateKeyIdentifier: string, clientEmail: string, clientIdentifier: string, createdAt: any, updatedAt: any } | null, androidAppBuildCredentialsList: Array<{ __typename?: 'AndroidAppBuildCredentials', id: string, isDefault: boolean, isLegacy: boolean, name: string, androidKeystore?: { __typename?: 'AndroidKeystore', id: string, type: AndroidKeystoreType, keystore: string, keystorePassword: string, keyAlias: string, keyPassword?: string | null, md5CertificateFingerprint?: string | null, sha1CertificateFingerprint?: string | null, sha256CertificateFingerprint?: string | null, createdAt: any, updatedAt: any } | null }> };
@@ -10011,6 +10186,18 @@ export type CommonIosAppCredentialsWithoutBuildCredentialsFragment = { __typenam
export type CommonIosAppCredentialsFragment = { __typename?: 'IosAppCredentials', id: string, iosAppBuildCredentialsList: Array<{ __typename?: 'IosAppBuildCredentials', id: string, iosDistributionType: IosDistributionType, distributionCertificate?: { __typename?: 'AppleDistributionCertificate', id: string, certificateP12?: string | null, certificatePassword?: string | null, serialNumber: string, developerPortalIdentifier?: string | null, validityNotBefore: any, validityNotAfter: any, updatedAt: any, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, iosAppBuildCredentialsList: Array<{ __typename?: 'IosAppBuildCredentials', id: string, iosAppCredentials: { __typename?: 'IosAppCredentials', id: string, app: { __typename?: 'App', id: string, name: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, githubRepository?: { __typename?: 'GitHubRepository', id: string, metadata: { __typename?: 'GitHubRepositoryMetadata', githubRepoOwnerName: string, githubRepoName: string } } | null }, appleAppIdentifier: { __typename?: 'AppleAppIdentifier', id: string, bundleIdentifier: string } }, provisioningProfile?: { __typename?: 'AppleProvisioningProfile', id: string, developerPortalIdentifier?: string | null } | null }> } | null, provisioningProfile?: { __typename?: 'AppleProvisioningProfile', id: string, expiration: any, developerPortalIdentifier?: string | null, provisioningProfile?: string | null, updatedAt: any, status: string, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, model?: string | null, deviceClass?: AppleDeviceClass | null, createdAt: any }> } | null }>, app: { __typename?: 'App', id: string, name: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, githubRepository?: { __typename?: 'GitHubRepository', id: string, metadata: { __typename?: 'GitHubRepositoryMetadata', githubRepoOwnerName: string, githubRepoName: string } } | null }, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, appleAppIdentifier: { __typename?: 'AppleAppIdentifier', id: string, bundleIdentifier: string }, pushKey?: { __typename?: 'ApplePushKey', id: string, keyIdentifier: string, updatedAt: any, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, iosAppCredentialsList: Array<{ __typename?: 'IosAppCredentials', id: string, app: { __typename?: 'App', id: string, name: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, githubRepository?: { __typename?: 'GitHubRepository', id: string, metadata: { __typename?: 'GitHubRepositoryMetadata', githubRepoOwnerName: string, githubRepoName: string } } | null }, appleAppIdentifier: { __typename?: 'AppleAppIdentifier', id: string, bundleIdentifier: string } }> } | null, appStoreConnectApiKeyForSubmissions?: { __typename?: 'AppStoreConnectApiKey', id: string, issuerIdentifier: string, keyIdentifier: string, name?: string | null, roles?: Array<AppStoreConnectUserRole> | null, createdAt: any, updatedAt: any, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null } | null };
export type ScheduleUpdateGroupDeletionMutationVariables = Exact<{
group: Scalars['ID']['input'];
}>;
export type ScheduleUpdateGroupDeletionMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', scheduleUpdateGroupDeletion: { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any } } };
export type MeUserActorQueryVariables = Exact<{ [key: string]: never; }>;
export type MeUserActorQuery = { __typename?: 'RootQuery', meUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null };
export type WorkerDeploymentFragment = { __typename?: 'WorkerDeployment', id: string, url: string, deploymentIdentifier: any, deploymentDomain: string, createdAt: any };
export type WorkerDeploymentAliasFragment = { __typename?: 'WorkerDeploymentAlias', id: string, aliasName?: any | null, url: string };
@@ -12,8 +12,6 @@ import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableVisibility,
LinkSharedEnvironmentVariableMutation,
UnlinkSharedEnvironmentVariableMutation,
UpdateEnvironmentVariableMutation,
} from '../generated';
import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
@@ -37,76 +35,6 @@ export type EnvironmentVariablePushInput = {
};
export const EnvironmentVariableMutation = {
async linkSharedEnvironmentVariableAsync(
graphqlClient: ExpoGraphqlClient,
environmentVariableId: string,
appId: string,
environment?: EnvironmentVariableEnvironment
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<LinkSharedEnvironmentVariableMutation>(
gql`
mutation LinkSharedEnvironmentVariable(
$appId: ID!
$environment: EnvironmentVariableEnvironment
$environmentVariableId: ID!
) {
environmentVariable {
linkSharedEnvironmentVariable(
appId: $appId
environmentVariableId: $environmentVariableId
environment: $environment
) {
id
...EnvironmentVariableFragment
}
}
}
${print(EnvironmentVariableFragmentNode)}
`,
{ appId, environment, environmentVariableId }
)
.toPromise()
);
return data.environmentVariable.linkSharedEnvironmentVariable;
},
async unlinkSharedEnvironmentVariableAsync(
graphqlClient: ExpoGraphqlClient,
environmentVariableId: string,
appId: string,
environment?: EnvironmentVariableEnvironment
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UnlinkSharedEnvironmentVariableMutation>(
gql`
mutation UnlinkSharedEnvironmentVariable(
$appId: ID!
$environment: EnvironmentVariableEnvironment
$environmentVariableId: ID!
) {
environmentVariable {
unlinkSharedEnvironmentVariable(
appId: $appId
environmentVariableId: $environmentVariableId
environment: $environment
) {
id
...EnvironmentVariableFragment
}
}
}
${print(EnvironmentVariableFragmentNode)}
`,
{ appId, environment, environmentVariableId }
)
.toPromise()
);
return data.environmentVariable.unlinkSharedEnvironmentVariable;
},
async createSharedVariableAsync(
graphqlClient: ExpoGraphqlClient,
input: CreateVariableArgs,
@@ -3,6 +3,8 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
CancelWorkflowRunMutation,
CancelWorkflowRunMutationVariables,
CreateWorkflowRunMutation,
CreateWorkflowRunMutationVariables,
WorkflowRevisionInput,
@@ -52,4 +54,31 @@ export namespace WorkflowRunMutation {
);
return { id: data.workflowRun.createWorkflowRun.id };
}
export async function cancelWorkflowRunAsync(
graphqlClient: ExpoGraphqlClient,
{
workflowRunId,
}: {
workflowRunId: string;
}
): Promise<void> {
await withErrorHandlingAsync(
graphqlClient
.mutation<CancelWorkflowRunMutation, CancelWorkflowRunMutationVariables>(
gql`
mutation CancelWorkflowRun($workflowRunId: ID!) {
workflowRun {
cancelWorkflowRun(workflowRunId: $workflowRunId) {
id
}
}
}
`,
{
workflowRunId,
}
)
.toPromise()
);
}
}
@@ -1,11 +1,23 @@
/* eslint-disable graphql/template-strings */
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import { AppByFullNameQuery, AppByIdQuery, AppFragment } from '../generated';
import {
AppByFullNameQuery,
AppByIdQuery,
AppByIdWorkflowRunsFilteredByStatusQuery,
AppByIdWorkflowsQuery,
AppFragment,
WorkflowFragment,
WorkflowRunFragment,
WorkflowRunStatus,
} from '../generated';
import { AppFragmentNode } from '../types/App';
import { WorkflowFragmentNode } from '../types/Workflow';
import { WorkflowRunFragmentNode } from '../types/WorkflowRun';
export const AppQuery = {
async byIdAsync(graphqlClient: ExpoGraphqlClient, projectId: string): Promise<AppFragment> {
@@ -60,4 +72,79 @@ export const AppQuery = {
assert(data.app, 'GraphQL: `app` not defined in server response');
return data.app.byFullName;
},
async byIdWorkflowsAsync(
graphqlClient: ExpoGraphqlClient,
appId: string
): Promise<WorkflowFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppByIdWorkflowsQuery>(
gql`
query AppByIdWorkflowsQuery($appId: String!) {
app {
byId(appId: $appId) {
id
workflows {
id
...WorkflowFragment
}
}
}
}
${print(WorkflowFragmentNode)}
`,
{ appId },
{ additionalTypenames: ['App'] }
)
.toPromise()
);
assert(data.app, 'GraphQL: `app` not defined in server response');
return data.app.byId.workflows;
},
async byIdWorkflowRunsFilteredByStatusAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
status?: WorkflowRunStatus,
limit?: number
): Promise<WorkflowRunFragment[]> {
validateLimit(limit);
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppByIdWorkflowRunsFilteredByStatusQuery>(
gql`
query AppByIdWorkflowRunsFilteredByStatusQuery(
$appId: String!
$status: WorkflowRunStatus
$limit: Int!
) {
app {
byId(appId: $appId) {
id
runs: workflowRunsPaginated(first: $limit, filter: { status: $status }) {
edges {
node {
id
...WorkflowRunFragment
}
}
}
}
}
}
${print(WorkflowRunFragmentNode)}
`,
{ appId, status, limit },
{ additionalTypenames: ['App'] }
)
.toPromise()
);
assert(data.app, 'GraphQL: `app` not defined in server response');
return data.app.byId.runs.edges.map(edge => edge.node);
},
};
function validateLimit(limit?: number): void {
assert(limit, 'limit is required');
assert(limit > 0, 'limit must be greater than 0');
assert(limit <= 100, 'limit must be less than or equal to 100');
}
@@ -30,6 +30,7 @@ export const BackgroundJobReceiptQuery = {
{ id: backgroundJobReceiptId },
{
additionalTypenames: ['BackgroundJobReceipt'],
requestPolicy: 'network-only',
}
)
.toPromise()
@@ -0,0 +1,141 @@
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
WorkflowRunByIdQuery,
WorkflowRunByIdQueryVariables,
WorkflowRunByIdWithJobsQuery,
WorkflowRunByIdWithJobsQueryVariables,
WorkflowRunFragment,
WorkflowRunStatus,
WorkflowRunsForAppIdFileNameAndStatusQuery,
} from '../generated';
import { WorkflowRunFragmentNode } from '../types/WorkflowRun';
export const WorkflowRunQuery = {
async byIdAsync(
graphqlClient: ExpoGraphqlClient,
workflowRunId: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<WorkflowRunByIdQuery, WorkflowRunByIdQueryVariables>(
gql`
query WorkflowRunById($workflowRunId: ID!) {
workflowRuns {
byId(workflowRunId: $workflowRunId) {
id
status
}
}
}
`,
{ workflowRunId },
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['WorkflowRun'],
}
)
.toPromise()
);
return data.workflowRuns.byId;
},
async withJobsByIdAsync(
graphqlClient: ExpoGraphqlClient,
workflowRunId: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<WorkflowRunByIdWithJobsQuery, WorkflowRunByIdWithJobsQueryVariables>(
gql`
query WorkflowRunByIdWithJobs($workflowRunId: ID!) {
workflowRuns {
byId(workflowRunId: $workflowRunId) {
id
name
status
createdAt
workflow {
id
name
fileName
}
jobs {
id
key
name
type
status
outputs
createdAt
}
}
}
}
`,
{ workflowRunId },
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['WorkflowRun'],
}
)
.toPromise()
);
return data.workflowRuns.byId;
},
async byAppIdFileNameAndStatusAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
fileName: string,
status?: WorkflowRunStatus,
limit?: number
): Promise<WorkflowRunFragment[]> {
validateLimit(limit);
const data = await withErrorHandlingAsync(
graphqlClient
.query<WorkflowRunsForAppIdFileNameAndStatusQuery>(
gql`
query WorkflowRunsForAppIdFileNameAndStatusQuery(
$appId: ID!
$fileName: String!
$status: WorkflowRunStatus
$limit: Int!
) {
workflows {
byAppIdAndFileName(appId: $appId, fileName: $fileName) {
id
runs: runsPaginated(first: $limit, filter: { status: $status }) {
edges {
node {
id
...WorkflowRunFragment
}
}
}
}
}
}
${print(WorkflowRunFragmentNode)}
`,
{ appId, fileName, status, limit },
{ additionalTypenames: ['Workflow'] }
)
.toPromise()
);
assert(data.workflows, 'GraphQL: `workflows` not defined in server response');
return data.workflows.byAppIdAndFileName.runs.edges.map(edge => edge.node);
},
};
function validateLimit(limit?: number): void {
assert(limit, 'limit is required');
assert(limit > 0, 'limit must be greater than 0');
assert(limit <= 100, 'limit must be less than or equal to 100');
}
@@ -12,6 +12,8 @@ export const UpdateFragmentNode = gql`
isRollBackToEmbedded
manifestPermalink
gitCommitHash
isGitWorkingTreeDirty
environment
actor {
__typename
id
@@ -34,6 +36,7 @@ export const UpdateFragmentNode = gql`
rolloutPercentage
rolloutControlUpdate {
id
group
}
fingerprint {
id
@@ -45,5 +48,7 @@ export const UpdateFragmentNode = gql`
isDebugFingerprint
}
}
manifestHostOverride
assetHostOverride
}
`;
@@ -0,0 +1,11 @@
import gql from 'graphql-tag';
export const WorkflowFragmentNode = gql`
fragment WorkflowFragment on Workflow {
id
name
fileName
createdAt
updatedAt
}
`;
@@ -0,0 +1,17 @@
import gql from 'graphql-tag';
export const WorkflowRunFragmentNode = gql`
fragment WorkflowRunFragment on WorkflowRun {
id
status
gitCommitMessage
gitCommitHash
createdAt
updatedAt
workflow {
id
name
fileName
}
}
`;
@@ -0,0 +1,232 @@
apply plugin: "com.android.application"
import com.android.build.OutputFile
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // https://reactnative.dev/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
enableHermes: false
]
apply from: '../../node_modules/react-native-unimodules/gradle.groovy'
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle"
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore.
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and mirrored here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "com.testapp"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
signingConfigs {
debug {
storeFile rootProject.file('keystores/debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
flavorDimensions = ["version"]
productFlavors {
development {
dimension "version"
applicationId "com.amanhimself.multiflavor"
}
production {
dimension "version"
applicationId "com.amanhimself.multiflavor.dark"
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
addUnimodulesDependencies()
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
apply from: "./eas-build.gradle"
@@ -69,7 +69,7 @@ describe(resolveGradleBuildContextAsync, () => {
} as any,
vcsClient
);
expect(gradleContext).toEqual({ moduleName: 'app', flavor: 'abc' });
expect(gradleContext).toEqual({ moduleName: 'app', flavor: 'abc', buildType: 'release' });
});
it('returns undefined if build.gradle does not exist', async () => {
@@ -131,6 +131,41 @@ describe(getAppBuildGradleAsync, () => {
},
});
});
test('parsing build.gradle with flavor dimensions as array', async () => {
vol.fromJSON(
{
'android/app/build.gradle': await fsReal.promises.readFile(
path.join(__dirname, 'fixtures/multiflavor-with-dimensions-array-build.gradle'),
'utf-8'
),
},
'/test'
);
const buildGradle = await getAppBuildGradleAsync('/test');
expect(
pick(buildGradle?.android ?? {}, ['defaultConfig', 'flavorDimensions', 'productFlavors'])
).toEqual({
defaultConfig: {
applicationId: 'com.testapp',
minSdkVersion: 'rootProject.ext.minSdkVersion',
targetSdkVersion: 'rootProject.ext.targetSdkVersion',
versionCode: '1',
versionName: '1.0',
},
flavorDimensions: ['version'],
productFlavors: {
development: {
applicationId: 'com.amanhimself.multiflavor',
dimension: 'version',
},
production: {
applicationId: 'com.amanhimself.multiflavor.dark',
dimension: 'version',
},
},
});
});
});
describe(parseGradleCommand, () => {
@@ -144,6 +179,12 @@ describe(parseGradleCommand, () => {
});
expect(result).toEqual({ moduleName: 'app', flavor: 'example', buildType: 'release' });
});
test('parsing :app:buildExampleStage', async () => {
const result = parseGradleCommand(':app:buildExampleStage', {
android: { productFlavors: { example: {} } },
});
expect(result).toEqual({ moduleName: 'app', flavor: 'example', buildType: 'stage' });
});
test('parsing :app:buildExampleDebug when flavor is named with uper-case', async () => {
const result = parseGradleCommand(':app:buildExampleRelease', {
android: { productFlavors: { Example: {} } },
@@ -166,6 +207,12 @@ describe(parseGradleCommand, () => {
const result = parseGradleCommand('bundleRelease --console verbose', {});
expect(result).toEqual({ buildType: 'release' });
});
test('parsing with array flavor dimensions', async () => {
const result = parseGradleCommand(':app:buildExampleRelease', {
android: { flavorDimensions: ['version'], productFlavors: { example: {} } },
});
expect(result).toEqual({ moduleName: 'app', flavor: 'example', buildType: 'release' });
});
});
describe(resolveConfigValue, () => {
@@ -9,6 +9,7 @@ import { Client } from '../../vcs/vcs';
export interface GradleBuildContext {
moduleName?: string;
flavor?: string;
buildType?: string;
}
export async function resolveGradleBuildContextAsync(
@@ -32,9 +33,11 @@ export async function resolveGradleBuildContextAsync(
`Building modules different than "${gradleUtils.DEFAULT_MODULE_NAME}" might result in unexpected behavior.`
);
}
return {
moduleName: parsedGradleCommand?.moduleName ?? gradleUtils.DEFAULT_MODULE_NAME,
flavor: parsedGradleCommand?.flavor,
buildType: parsedGradleCommand?.buildType,
};
} else {
return { moduleName: gradleUtils.DEFAULT_MODULE_NAME };
@@ -20,7 +20,14 @@ interface Config {
interface AppBuildGradle {
android?: {
defaultConfig?: Config;
flavorDimensions?: string; // e.g. '"dimension1", "dimension2"'
/**
* If defined as `flavorDimensions = ['dimension1', 'dimension2']`,
* this will be an array of strings (`['dimension1', 'dimension2']`).
*
* If defined as `flavorDimensions "dimension1", "dimension2"`,
* this will be a string (`"dimension1", "dimension2"`).
*/
flavorDimensions?: string | string[];
productFlavors?: Record<string, Config>;
};
}
@@ -65,7 +72,11 @@ export function resolveConfigValue(
* the flavor name
**/
export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): GradleCommand {
const hasFlavorDimensions = (buildGradle.android?.flavorDimensions ?? '').split(',').length > 1;
const flavorDimensions =
typeof buildGradle.android?.flavorDimensions === 'string'
? buildGradle.android.flavorDimensions.split(',')
: buildGradle.android?.flavorDimensions;
const hasFlavorDimensions = flavorDimensions && flavorDimensions.length > 1;
if (hasFlavorDimensions) {
throw new Error('flavorDimensions in build.gradle are not supported yet');
}
@@ -82,7 +93,7 @@ export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): Gr
const [moduleName, taskName] =
splitCmd.length > 1 ? [splitCmd[0], splitCmd[1]] : [undefined, splitCmd[0]];
const matchResult = taskName.match(/(build|bundle|assemble|package)(.*)(Release|Debug)/);
const matchResult = taskName.match(/(build|bundle|assemble|package)(.*)([A-Z][a-z]+)/);
if (!matchResult) {
throw new Error(`Failed to parse gradle command: ${cmd}`);
}
@@ -104,9 +115,14 @@ export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): Gr
throw new Error(`flavor ${firstLetter.toLowerCase().concat(rest)} is not defined`);
}
}
const buildType = matchResult[3]
? matchResult[3].charAt(0).toLowerCase() + matchResult[3].slice(1)
: undefined;
return {
moduleName,
flavor,
buildType: matchResult[3] ? matchResult[3].toLowerCase() : undefined,
buildType,
};
}
+7 -9
View File
@@ -13,19 +13,13 @@ import Log, { learnMore } from '../log';
import { Actor } from '../user/User';
import { expoCommandAsync } from '../utils/expoCli';
export function getUsername(exp: ExpoConfig, user: Actor): string | undefined {
export function getUsernameForBuildMetadataAndBuildJob(user: Actor): string | undefined {
switch (user.__typename) {
case 'User':
return user.username;
case 'SSOUser':
return user.username;
case 'Robot':
// owner field is necessary to run `expo prebuild`
if (!exp.owner) {
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
);
}
// robot users don't have usernames
return undefined;
}
@@ -84,8 +78,12 @@ export function isExpoUpdatesInstalledOrAvailable(
return isExpoUpdatesInstalled(projectDir);
}
export function isUsingEASUpdate(exp: ExpoConfig, projectId: string): boolean {
return exp.updates?.url === getEASUpdateURL(projectId);
export function isUsingEASUpdate(
exp: ExpoConfig,
projectId: string,
manifestHostOverride: string | null
): boolean {
return exp.updates?.url === getEASUpdateURL(projectId, manifestHostOverride);
}
export async function getExpoUpdatesPackageVersionIfInstalledAsync(
+42 -22
View File
@@ -1073,27 +1073,47 @@ export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync(
runtimeToPlatformsAndFingerprintInfoMapping.map(r => [r.runtimeVersion, r.platforms])
);
return await mapMapAsync(runtimeToPlatformsMap, async (platforms, runtimeVersion) => {
return Object.fromEntries(
await Promise.all(
platforms.map<Promise<[string, UpdateRolloutInfo]>>(async platform => {
const updateIdForPlatform = await BranchQuery.getLatestUpdateIdOnBranchAsync(
graphqlClient,
{
appId,
branchName,
runtimeVersion,
platform: updatePublishPlatformToAppPlatform[platform],
}
);
if (!updateIdForPlatform) {
throw new Error(
`No updates on branch ${branchName} for platform ${platform} and runtimeVersion ${runtimeVersion} to roll out from.`
);
}
return [platform, { rolloutPercentage, rolloutControlUpdateId: updateIdForPlatform }];
})
)
);
return await getUpdateRolloutInfoGroupAsync(graphqlClient, {
appId,
branchName,
rolloutPercentage,
runtimeVersion,
platforms,
});
});
}
export async function getUpdateRolloutInfoGroupAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
branchName,
rolloutPercentage,
runtimeVersion,
platforms,
}: {
appId: string;
branchName: string;
rolloutPercentage: number;
runtimeVersion: string;
platforms: UpdatePublishPlatform[];
}
): Promise<UpdateRolloutInfoGroup> {
// note that this could return control updates in different update groups if the update groups only have a single platform
return Object.fromEntries(
await Promise.all(
platforms.map<Promise<[string, UpdateRolloutInfo]>>(async platform => {
const updateIdForPlatform = await BranchQuery.getLatestUpdateIdOnBranchAsync(
graphqlClient,
{
appId,
branchName,
runtimeVersion,
platform: updatePublishPlatformToAppPlatform[platform],
}
);
return [platform, { rolloutPercentage, rolloutControlUpdateId: updateIdForPlatform }];
})
)
);
}
@@ -70,7 +70,7 @@ async function assertSimulatorAppInstalledAsync(): Promise<void> {
const simulatorAppId = await getSimulatorAppIdAsync();
if (!simulatorAppId) {
throw new Error(
`Can't determine id of Simulator app; the Simulator is most likely not installed on this machine. Run 'sudo xcode-select -s /Applications/Xcode.app'`
`Can't determine id of Simulator app; the Simulator is most likely not installed on this machine. Run 'sudo xcode-select -s /Applications/Xcode.app/Contents/Developer'`
);
}
@@ -124,6 +124,7 @@ describe(AndroidSubmitCommand, () => {
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -163,6 +164,7 @@ describe(AndroidSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -210,6 +212,7 @@ describe(AndroidSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -260,6 +263,7 @@ describe(AndroidSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -325,6 +329,7 @@ describe(AndroidSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -388,6 +393,7 @@ describe(AndroidSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -458,6 +464,7 @@ describe(AndroidSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -157,6 +157,7 @@ describe(getServiceAccountKeyResultAsync, () => {
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -200,6 +201,7 @@ describe(getServiceAccountKeyResultAsync, () => {
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -239,6 +241,7 @@ describe(getServiceAccountKeyResultAsync, () => {
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
+18
View File
@@ -22,6 +22,7 @@ export interface SubmissionContext<T extends Platform> {
exp: ExpoConfig;
nonInteractive: boolean;
isVerboseFastlaneEnabled: boolean;
groups: T extends Platform.IOS ? string[] : undefined;
platform: T;
profile: SubmitProfile<T>;
projectDir: string;
@@ -32,6 +33,7 @@ export interface SubmissionContext<T extends Platform> {
analytics: Analytics;
vcsClient: Client;
applicationIdentifierOverride?: string;
whatToTest?: string;
specifiedProfile?: string;
}
@@ -48,6 +50,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
env?: Record<string, string>;
nonInteractive: boolean;
isVerboseFastlaneEnabled: boolean;
groups: string[] | undefined;
platform: T;
profile: SubmitProfile<T>;
projectDir: string;
@@ -58,6 +61,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
exp: ExpoConfig;
projectId: string;
vcsClient: Client;
whatToTest?: string;
specifiedProfile?: string;
}): Promise<SubmissionContext<T>> {
const {
@@ -70,6 +74,9 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
graphqlClient,
analytics,
vcsClient,
profile,
groups: groupsFromParams,
platform,
} = params;
const { env, ...rest } = params;
const projectName = exp.slug;
@@ -88,6 +95,16 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
});
}
let groups;
if (platform === Platform.IOS) {
groups = (groupsFromParams ??
(profile as SubmitProfile<Platform.IOS>).groups ??
[]) as T extends Platform.IOS ? string[] : undefined;
} else {
groups = undefined as T extends Platform.IOS ? string[] : undefined;
}
const analyticsEventProperties = {
tracking_id: uuidv4(),
platform: params.platform,
@@ -101,6 +118,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
...rest,
accountName: account.name,
credentialsCtx,
groups,
projectName,
user: actor,
analyticsEventProperties,
@@ -125,11 +125,13 @@ export default class IosSubmitter extends BaseSubmitter<
): IosSubmissionConfigInput {
const { appSpecificPassword, ascApiKeyResult } = credentials;
const { appleIdUsername, ascAppIdentifier } = options;
const { isVerboseFastlaneEnabled } = this.ctx;
const { isVerboseFastlaneEnabled, groups, whatToTest } = this.ctx;
return {
ascAppIdentifier,
appleIdUsername,
isVerboseFastlaneEnabled,
groups,
changelog: whatToTest,
...(appSpecificPassword ? this.formatAppSpecificPassword(appSpecificPassword) : null),
...(ascApiKeyResult?.result ? this.formatAscApiKeyResult(ascApiKeyResult.result) : null),
};
@@ -64,6 +64,7 @@ async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platfor
exp: testProject.appJSON.expo,
projectId,
vcsClient,
groups: undefined,
});
}
@@ -251,6 +252,7 @@ describe(getAscApiKeyResultAsync, () => {
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -101,6 +101,7 @@ describe(IosSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -136,6 +137,7 @@ describe(IosSubmitCommand, () => {
},
nonInteractive: true,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -174,6 +176,8 @@ describe(IosSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: ['Test'],
whatToTest: 'my changelog',
actor: mockJester,
graphqlClient,
analytics,
@@ -193,6 +197,8 @@ describe(IosSubmitCommand, () => {
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '12345678',
isVerboseFastlaneEnabled: false,
groups: ['Test'],
changelog: 'my changelog',
},
submittedBuildId: undefined,
});
@@ -240,6 +246,7 @@ describe(IosSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -259,6 +266,7 @@ describe(IosSubmitCommand, () => {
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '87654321',
isVerboseFastlaneEnabled: false,
groups: [],
},
archiveSource: undefined,
});
@@ -300,6 +308,7 @@ describe(IosSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -319,6 +328,7 @@ describe(IosSubmitCommand, () => {
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '12345678',
isVerboseFastlaneEnabled: false,
groups: [],
},
archiveSource: undefined,
});
@@ -365,6 +375,7 @@ describe(IosSubmitCommand, () => {
},
nonInteractive: false,
isVerboseFastlaneEnabled: false,
groups: [],
actor: mockJester,
graphqlClient,
analytics,
@@ -385,6 +396,7 @@ describe(IosSubmitCommand, () => {
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '12345678',
isVerboseFastlaneEnabled: false,
groups: [],
},
archiveSource: undefined,
});
+14 -7
View File
@@ -98,17 +98,19 @@ async function ensureEASUpdatesIsConfiguredInExpoConfigAsync({
projectDir,
platform,
workflows,
manifestHostOverride,
}: {
exp: ExpoConfig;
projectId: string;
projectDir: string;
platform: RequestedPlatform;
workflows: Record<Platform, Workflow>;
manifestHostOverride: string | null;
}): Promise<{ projectChanged: boolean; exp: ExpoConfig }> {
const modifyConfig: Partial<ExpoConfig> = {};
if (exp.updates?.url !== getEASUpdateURL(projectId)) {
modifyConfig.updates = { url: getEASUpdateURL(projectId) };
if (exp.updates?.url !== getEASUpdateURL(projectId, manifestHostOverride)) {
modifyConfig.updates = { url: getEASUpdateURL(projectId, manifestHostOverride) };
}
let androidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
@@ -303,20 +305,22 @@ async function ensureEASUpdateIsConfiguredNativelyAsync(
export async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
// Skip if eas.json doesn't exist.
if (!(await fs.pathExists(easJsonPath))) {
Log.warn(
`EAS Build is not configured. If you'd like to use EAS Build with EAS Update, run ${chalk.bold(
'eas build:configure'
)}.`
);
return;
}
// Add channel to all build profiles.
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
// If there are no build profiles then we are done.
if (!easJsonRawObject.build) {
return easJsonRawObject;
}
const easBuildProfilesWithChannels = Object.keys(easJsonRawObject.build).reduce(
(acc, profileNameKey) => {
const buildProfile = easJsonRawObject.build[profileNameKey];
@@ -372,6 +376,7 @@ export async function ensureEASUpdateIsConfiguredAsync({
platform,
env,
forceNativeConfigSync,
manifestHostOverride,
}: {
exp: ExpoConfig;
projectId: string;
@@ -380,6 +385,7 @@ export async function ensureEASUpdateIsConfiguredAsync({
platform: RequestedPlatform | null;
env: Env | undefined;
forceNativeConfigSync?: boolean;
manifestHostOverride: string | null;
}): Promise<void> {
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(
projectDir,
@@ -409,6 +415,7 @@ export async function ensureEASUpdateIsConfiguredAsync({
projectId,
platform,
workflows,
manifestHostOverride,
});
if (forceNativeConfigSync || projectChanged || !hasExpoUpdates) {
+39
View File
@@ -0,0 +1,39 @@
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../graphql/client';
import {
BackgroundJobReceiptDataFragment,
ScheduleUpdateGroupDeletionMutation,
ScheduleUpdateGroupDeletionMutationVariables,
} from '../graphql/generated';
import { BackgroundJobReceiptNode } from '../graphql/types/BackgroundJobReceipt';
export async function scheduleUpdateGroupDeletionAsync(
graphqlClient: ExpoGraphqlClient,
{
group,
}: {
group: string;
}
): Promise<BackgroundJobReceiptDataFragment> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<ScheduleUpdateGroupDeletionMutation, ScheduleUpdateGroupDeletionMutationVariables>(
gql`
mutation ScheduleUpdateGroupDeletion($group: ID!) {
update {
scheduleUpdateGroupDeletion(group: $group) {
id
...BackgroundJobReceiptData
}
}
}
${BackgroundJobReceiptNode}
`,
{ group }
)
.toPromise()
);
return result.update.scheduleUpdateGroupDeletion;
}
+91
View File
@@ -10,21 +10,27 @@ import {
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import {
AppPlatform,
RuntimeFragment,
UpdateFragment,
ViewUpdateGroupsOnAppQueryVariables,
ViewUpdateGroupsOnBranchQueryVariables,
} from '../graphql/generated';
import { RuntimeQuery } from '../graphql/queries/RuntimeQuery';
import { UpdateQuery } from '../graphql/queries/UpdateQuery';
import Log from '../log';
import { UpdatePublishPlatform } from '../project/publish';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { Connection, QueryParams, selectPaginatedAsync } from '../utils/relay';
export const UPDATES_LIMIT = 50;
export const UPDATE_GROUPS_LIMIT = 25;
export const RUNTIME_VERSIONS_LIMIT = 25;
export async function listAndRenderUpdateGroupsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
@@ -100,6 +106,58 @@ export async function listAndRenderUpdateGroupsOnBranchAsync(
}
}
export async function selectRuntimeAndGetLatestUpdateGroupForEachPublishPlatformOnBranchAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
branchName,
paginatedQueryOptions,
}: {
projectId: string;
branchName: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<Record<UpdatePublishPlatform, UpdateFragment[] | undefined>> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select an update in non-interactive mode.');
}
const runtimeVersion = await selectRuntimeOnBranchAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (!runtimeVersion) {
throw new Error('No runtime version selected.');
}
return {
ios: (
await queryUpdateGroupsOnBranchAsync(graphqlClient, {
appId: projectId,
branchName,
limit: 1,
offset: 0,
filter: {
runtimeVersions: [runtimeVersion.version],
platform: AppPlatform.Ios,
},
})
)[0],
android: (
await queryUpdateGroupsOnBranchAsync(graphqlClient, {
appId: projectId,
branchName,
limit: 1,
offset: 0,
filter: {
runtimeVersions: [runtimeVersion.version],
platform: AppPlatform.Android,
},
})
)[0],
};
}
export async function selectUpdateGroupOnBranchAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -230,3 +288,36 @@ function renderUpdateGroupsOnApp({
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
export async function selectRuntimeOnBranchAsync(
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,
});
}
+394 -1
View File
@@ -2,14 +2,26 @@ import { ExpoConfig } from '@expo/config';
import assert from 'assert';
import nullthrows from 'nullthrows';
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from './getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
import {
selectRuntimeAndGetLatestUpdateGroupForEachPublishPlatformOnBranchAsync,
selectUpdateGroupOnBranchAsync,
} from './queries';
import { truncateString as truncateUpdateMessage } from './utils';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getUpdateGroupUrl } from '../build/utils/url';
import { selectChannelOnAppAsync } from '../channel/queries';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { getPaginatedQueryOptions } from '../commandUtils/pagination';
import fetch from '../fetch';
import { UpdateFragment, UpdateInfoGroup } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../graphql/queries/UpdateQuery';
import Log, { link } from '../log';
import { ora } from '../ora';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { UpdatePublishPlatform, getUpdateRolloutInfoGroupAsync } from '../project/publish';
import { promptAsync } from '../prompts';
import {
CodeSigningInfo,
checkDirectiveBodyAgainstUpdateInfoGroup,
@@ -39,6 +51,7 @@ export async function republishAsync({
updateMessage,
codeSigningInfo,
json,
rolloutPercentage,
}: {
graphqlClient: ExpoGraphqlClient;
app: { exp: ExpoConfig; projectId: string };
@@ -47,6 +60,7 @@ export async function republishAsync({
updateMessage: string;
codeSigningInfo?: CodeSigningInfo;
json?: boolean;
rolloutPercentage?: number;
}): Promise<void> {
const { branchName: targetBranchName, branchId: targetBranchId } = targetBranch;
@@ -57,7 +71,9 @@ export async function republishAsync({
update.groupId === arbitraryUpdate.groupId &&
update.branchId === arbitraryUpdate.branchId &&
update.branchName === arbitraryUpdate.branchName &&
update.runtimeVersion === arbitraryUpdate.runtimeVersion;
update.runtimeVersion === arbitraryUpdate.runtimeVersion &&
update.manifestHostOverride === arbitraryUpdate.manifestHostOverride &&
update.assetHostOverride === arbitraryUpdate.assetHostOverride;
assert(
updatesToPublish.every(isSameGroup),
'All updates being republished must belong to the same update group'
@@ -139,6 +155,15 @@ export async function republishAsync({
];
})
),
rolloutInfoGroup: rolloutPercentage
? await getUpdateRolloutInfoGroupAsync(graphqlClient, {
appId: app.projectId,
branchName: targetBranchName,
runtimeVersion,
rolloutPercentage,
platforms: updatesToPublish.map(update => update.platform as UpdatePublishPlatform),
})
: null,
};
updatesRepublished = await PublishMutation.publishUpdateGroupAsync(graphqlClient, [
@@ -148,7 +173,11 @@ export async function republishAsync({
message: updateMessage,
...objectToMergeIn,
gitCommitHash: updatesToPublish[0].gitCommitHash,
isGitWorkingTreeDirty: updatesToPublish[0].isGitWorkingTreeDirty,
environment: updatesToPublish[0].environment,
awaitingCodeSigningInfo: !!codeSigningInfo,
manifestHostOverride: updatesToPublish[0].manifestHostOverride,
assetHostOverride: updatesToPublish[0].assetHostOverride,
},
]);
@@ -226,8 +255,372 @@ export async function republishAsync({
...(updatesRepublishedByPlatform.ios
? [{ label: 'iOS update ID', value: updatesRepublishedByPlatform.ios.id }]
: []),
...(updatesRepublishedByPlatform.android?.rolloutControlUpdate
? [
{
label: 'Android Rollout',
value: `${updatesRepublishedByPlatform.android?.rolloutPercentage}% (Base update ID: ${updatesRepublishedByPlatform.android?.rolloutControlUpdate.id})`,
},
]
: []),
...(updatesRepublishedByPlatform.ios?.rolloutControlUpdate
? [
{
label: 'iOS Rollout',
value: `${updatesRepublishedByPlatform.ios?.rolloutPercentage}% (Base update ID: ${updatesRepublishedByPlatform.ios?.rolloutControlUpdate.id})`,
},
]
: []),
{ label: 'Message', value: updateMessage },
{ label: 'EAS Dashboard', value: link(updateGroupUrl, { dim: false }) },
])
);
}
type GetUpdateOrAskForUpdatesOptions = {
nonInteractive: boolean;
json: boolean;
groupId?: string;
branchName?: string;
channelName?: string;
};
export async function getUpdateGroupAsync(
graphqlClient: ExpoGraphqlClient,
groupId: string
): Promise<UpdateToRepublish[]> {
const updateGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId,
});
return updateGroup.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
}));
}
type AskUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionOptions = {
nonInteractive: boolean;
json: boolean;
branchName?: string;
channelName?: string;
};
export async function askUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
options: AskUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionOptions
): Promise<Record<UpdatePublishPlatform, UpdateToRepublish[] | undefined>> {
if (options.nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
if (options.branchName) {
return await askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(
graphqlClient,
{
...options,
branchName: options.branchName,
projectId,
}
);
}
if (options.channelName) {
return await askUpdateGroupForEachPublishPlatformFromChannelNameFilteringByRuntimeVersionAsync(
graphqlClient,
{
...options,
channelName: options.channelName,
projectId,
}
);
}
const { choice } = await promptAsync({
type: 'select',
message: 'Find update by branch or channel?',
name: 'choice',
choices: [
{ title: 'Branch', value: 'branch' },
{ title: 'Channel', value: 'channel' },
],
});
if (choice === 'channel') {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel to view',
paginatedQueryOptions: {
json: options.json,
nonInteractive: options.nonInteractive,
offset: 0,
},
});
return await askUpdateGroupForEachPublishPlatformFromChannelNameFilteringByRuntimeVersionAsync(
graphqlClient,
{
...options,
channelName: name,
projectId,
}
);
} else if (choice === 'branch') {
const { name } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Select branch from which to choose update',
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: options.json,
nonInteractive: options.nonInteractive,
offset: 0,
},
});
return await askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(
graphqlClient,
{
...options,
branchName: name,
projectId,
}
);
} else {
throw new Error('Must choose update via channel or branch');
}
}
export async function getUpdateGroupOrAskForUpdateGroupAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
options: GetUpdateOrAskForUpdatesOptions
): Promise<UpdateToRepublish[]> {
if (options.groupId) {
return await getUpdateGroupAsync(graphqlClient, options.groupId);
}
if (options.nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
if (options.branchName) {
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...options,
branchName: options.branchName,
projectId,
});
}
if (options.channelName) {
return await askUpdatesFromChannelNameAsync(graphqlClient, {
...options,
channelName: options.channelName,
projectId,
});
}
const { choice } = await promptAsync({
type: 'select',
message: 'Find update by branch or channel?',
name: 'choice',
choices: [
{ title: 'Branch', value: 'branch' },
{ title: 'Channel', value: 'channel' },
],
});
if (choice === 'channel') {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel to view',
paginatedQueryOptions: {
json: options.json,
nonInteractive: options.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromChannelNameAsync(graphqlClient, {
...options,
channelName: name,
projectId,
});
} else if (choice === 'branch') {
const { name } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Select branch from which to choose update',
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: options.json,
nonInteractive: options.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...options,
branchName: name,
projectId,
});
} else {
throw new Error('Must choose update via channel or branch');
}
}
async function askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
branchName,
json,
nonInteractive,
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
): Promise<Record<UpdatePublishPlatform, UpdateToRepublish[] | undefined>> {
const publishPlatformToLatestUpdateGroup =
await selectRuntimeAndGetLatestUpdateGroupForEachPublishPlatformOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions: getPaginatedQueryOptions({ json, 'non-interactive': nonInteractive }),
});
return {
ios: publishPlatformToLatestUpdateGroup.ios?.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
})),
android: publishPlatformToLatestUpdateGroup.android?.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
})),
};
}
async function askUpdatesFromBranchNameAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
branchName,
json,
nonInteractive,
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
const updateGroup = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions: getPaginatedQueryOptions({ json, 'non-interactive': nonInteractive }),
});
return updateGroup.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
}));
}
async function askUpdateGroupForEachPublishPlatformFromChannelNameFilteringByRuntimeVersionAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
channelName,
json,
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<Record<UpdatePublishPlatform, UpdateToRepublish[] | undefined>> {
const { branchName } = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
channelName
);
return await askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(
graphqlClient,
{
projectId,
branchName,
json,
nonInteractive,
}
);
}
async function askUpdatesFromChannelNameAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
channelName,
json,
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
const { branchName } = await getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync(
graphqlClient,
projectId,
channelName
);
return await askUpdatesFromBranchNameAsync(graphqlClient, {
projectId,
branchName,
json,
nonInteractive,
});
}
type GetOrAskUpdateMessageOptions = {
updateMessage?: string;
nonInteractive: boolean;
json: boolean;
};
/**
* Get or ask the user for the update (group) message for the republish
*/
export async function getOrAskUpdateMessageAsync(
updateGroup: UpdateToRepublish[],
options: GetOrAskUpdateMessageOptions
): Promise<string> {
if (options.updateMessage) {
return sanitizeUpdateMessage(options.updateMessage);
}
if (options.nonInteractive || options.json) {
throw new Error('Must supply --message when in non-interactive mode');
}
// This command only uses a single update group to republish, meaning these values are always identical
const oldGroupId = updateGroup[0].groupId;
const oldUpdateMessage = updateGroup[0].message;
const { updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: 'Provide an update message.',
initial: `Republish "${oldUpdateMessage!}" - group: ${oldGroupId}`,
validate: (value: any) => (value ? true : 'Update message may not be empty.'),
});
return sanitizeUpdateMessage(updateMessage);
}
function sanitizeUpdateMessage(updateMessage: string): string {
if (updateMessage !== truncateUpdateMessage(updateMessage, 1024)) {
Log.warn('Update message exceeds the allowed 1024 character limit, truncated update message.');
return truncateUpdateMessage(updateMessage, 1024);
}
return updateMessage;
}
@@ -0,0 +1,203 @@
import { ExpoConfig } from '@expo/config';
import nullthrows from 'nullthrows';
import { getUpdateJsonInfosForUpdates } from './utils';
import { getUpdateGroupUrl } from '../build/utils/url';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import fetch from '../fetch';
import { PublishUpdateGroupInput, UpdatePublishMutation } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import Log, { link } from '../log';
import { ora } from '../ora';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import {
RuntimeVersionInfo,
UpdatePublishPlatform,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
} from '../project/publish';
import {
CodeSigningInfo,
checkDirectiveBodyAgainstUpdateInfoGroup,
getDirectiveBodyAsync,
signBody,
} from '../utils/code-signing';
import uniqBy from '../utils/expodash/uniqBy';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
export async function publishRollBackToEmbeddedUpdateAsync({
graphqlClient,
projectId,
exp,
updateMessage,
branch,
codeSigningInfo,
platforms,
runtimeVersion,
json,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
exp: ExpoConfig;
updateMessage: string | undefined;
branch: { name: string; id: string };
codeSigningInfo: CodeSigningInfo | undefined;
platforms: UpdatePublishPlatform[];
runtimeVersion: string;
json: boolean;
}): Promise<void> {
const runtimeToPlatformsAndFingerprintInfoMapping =
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects(
platforms.map(platform => ({
platform,
runtimeVersionInfo: {
runtimeVersion,
expoUpdatesRuntimeFingerprint: null,
expoUpdatesRuntimeFingerprintHash: null,
},
}))
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await publishRollbacksAsync({
graphqlClient,
updateMessage,
branchId: branch.id,
codeSigningInfo,
runtimeToPlatformsAndFingerprintInfoMapping,
platforms,
});
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to publish updates');
throw e;
}
if (json) {
printJsonOnlyOutput(getUpdateJsonInfosForUpdates(newUpdates));
} else {
Log.addNewLineIfNone();
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
version => version.runtimeVersion
)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
Log.log(
formatFields([
{ label: 'Branch', value: branch.name },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate ? [{ label: 'Android update ID', value: newAndroidUpdate.id }] : []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: updateMessage ?? '' },
{ label: 'EAS Dashboard', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
}
}
}
async function publishRollbacksAsync({
graphqlClient,
updateMessage,
branchId,
codeSigningInfo,
runtimeToPlatformsAndFingerprintInfoMapping,
platforms,
}: {
graphqlClient: ExpoGraphqlClient;
updateMessage: string | undefined;
branchId: string;
codeSigningInfo: CodeSigningInfo | undefined;
runtimeToPlatformsAndFingerprintInfoMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
})[];
platforms: UpdatePublishPlatform[];
}): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
const rollbackInfoGroups = Object.fromEntries(platforms.map(platform => [platform, true]));
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformsAndFingerprintInfoMapping.map(
({ runtimeVersion, platforms }) => {
const localRollbackInfoGroup = Object.fromEntries(
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
);
return {
branchId,
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
runtimeVersion,
message: updateMessage,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
const newUpdates = await PublishMutation.publishUpdateGroupAsync(graphqlClient, updateGroups);
if (codeSigningInfo) {
Log.log('🔒 Signing roll back');
const updatesTemp = [...newUpdates];
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
const newUpdates = updatesTemp.splice(
0,
Object.keys(nullthrows(updateGroup.rollBackToEmbeddedInfoGroup)).length
);
return {
updateGroup,
newUpdates,
};
});
await Promise.all(
updateGroupsAndTheirUpdates.map(async ({ newUpdates }) => {
await Promise.all(
newUpdates.map(async newUpdate => {
const response = await fetch(newUpdate.manifestPermalink, {
method: 'GET',
headers: { accept: 'multipart/mixed' },
});
const directiveBody = nullthrows(await getDirectiveBodyAsync(response));
checkDirectiveBodyAgainstUpdateInfoGroup(directiveBody);
const directiveSignature = signBody(directiveBody, codeSigningInfo);
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
keyid: codeSigningInfo.codeSigningMetadata.keyid,
sig: directiveSignature,
});
})
);
})
);
}
return newUpdates;
}
-11
View File
@@ -3,7 +3,6 @@ import { format } from '@expo/timeago.js';
import chalk from 'chalk';
import dateFormat from 'dateformat';
import { getEASUpdateURL } from '../api';
import {
Robot,
SsoUser,
@@ -270,13 +269,3 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra
},
};
}
export async function checkEASUpdateURLIsSetAsync(
exp: ExpoConfig,
projectId: string
): Promise<boolean> {
const configuredURL = exp.updates?.url;
const expectedURL = getEASUpdateURL(projectId);
return configuredURL === expectedURL;
}
+24 -18
View File
@@ -1,6 +1,8 @@
import gql from 'graphql-tag';
import { createGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../graphql/client';
import { MeUserActorQuery, MeUserActorQueryVariables } from '../graphql/generated';
export async function fetchUserAsync({
sessionSecret,
@@ -8,26 +10,30 @@ export async function fetchUserAsync({
sessionSecret: string;
}): Promise<{ id: string; username: string }> {
const graphqlClient = createGraphqlClient({ accessToken: null, sessionSecret });
const result = await graphqlClient
.query(
gql`
query UserQuery {
meUserActor {
id
username
const result = await withErrorHandlingAsync(
graphqlClient
.query<MeUserActorQuery, MeUserActorQueryVariables>(
gql`
query MeUserActorQuery {
meUserActor {
id
username
}
}
}
`,
{},
{
additionalTypenames: [] /* UserQuery has immutable fields */,
}
)
.toPromise();
`,
{},
{ additionalTypenames: ['UserActor'] }
)
.toPromise()
);
const meUserActor = result.meUserActor;
if (!meUserActor) {
throw new Error('Failed to fetch user data after login.');
}
const { data } = result;
return {
id: data.meUserActor.id,
username: data.meUserActor.username,
id: meUserActor.id,
username: meUserActor.username,
};
}
@@ -3,6 +3,8 @@ import Log from '../../log';
jest.mock('chalk', () => ({
red: jest.fn(text => `red(${text})`),
blue: jest.fn(text => `blue(${text})`),
yellow: jest.fn(text => `yellow(${text})`),
green: jest.fn(text => `green(${text})`),
gray: jest.fn(text => `gray(${text})`),
cyan: jest.fn(text => `cyan(${text})`),
+22
View File
@@ -1,5 +1,6 @@
import { IOSConfig } from '@expo/config-plugins';
import plist from '@expo/plist';
import binaryPlist from 'bplist-parser';
import fs from 'fs-extra';
import path from 'path';
@@ -25,3 +26,24 @@ export async function writePlistAsync(
await fs.mkdirp(path.dirname(plistPath));
await fs.writeFile(plistPath, contents);
}
const CHAR_CHEVRON_OPEN = 60;
const CHAR_B_LOWER = 98;
export function parseBinaryPlistBuffer(contents: Buffer): any {
if (contents[0] === CHAR_CHEVRON_OPEN) {
const info = plist.parse(contents.toString());
if (Array.isArray(info)) {
return info[0];
}
return info;
} else if (contents[0] === CHAR_B_LOWER) {
const info = binaryPlist.parseBuffer(contents);
if (Array.isArray(info)) {
return info[0];
}
return info;
} else {
throw new Error(`Cannot parse plist of type byte (0x${contents[0].toString(16)})`);
}
}
@@ -26,6 +26,7 @@ const humanReadableServiceName: Record<StatuspageServiceName, string> = {
[StatuspageServiceName.EasUpdate]: 'EAS Update',
[StatuspageServiceName.GithubApiRequests]: 'GitHub API Requests',
[StatuspageServiceName.GithubWebhooks]: 'Github Webhooks',
[StatuspageServiceName.EasWorkflows]: 'EAS Workflows',
};
function warnAboutServiceOutage(service: StatuspageServiceFragment): void {
+73 -63
View File
@@ -104,7 +104,7 @@ export default class GitClient extends Client {
commitAllFiles?: boolean;
nonInteractive: boolean;
}): Promise<void> {
await ensureGitConfiguredAsync({ nonInteractive });
await this.ensureGitConfiguredAsync({ nonInteractive });
try {
if (commitAllFiles) {
@@ -391,74 +391,84 @@ export default class GitClient extends Client {
public override canGetLastCommitMessage(): boolean {
return true;
}
}
async function ensureGitConfiguredAsync({
nonInteractive,
}: {
nonInteractive: boolean;
}): Promise<void> {
let usernameConfigured = true;
let emailConfigured = true;
try {
await spawnAsync('git', ['config', '--get', 'user.name']);
} catch (err: any) {
Log.debug(err);
usernameConfigured = false;
}
try {
await spawnAsync('git', ['config', '--get', 'user.email']);
} catch (err: any) {
Log.debug(err);
emailConfigured = false;
}
if (usernameConfigured && emailConfigured) {
return;
}
Log.warn(
`You need to configure Git with your ${[
!usernameConfigured && 'username (user.name)',
!emailConfigured && 'email address (user.email)',
]
.filter(i => i)
.join(' and ')}`
);
if (nonInteractive) {
throw new Error('Git cannot be configured automatically in non-interactive mode');
}
if (!usernameConfigured) {
const { username } = await promptAsync({
type: 'text',
name: 'username',
message: 'Username:',
validate: (input: string) => input !== '',
});
const spinner = ora(
`Running ${chalk.bold(`git config --local user.name ${username}`)}`
).start();
private async ensureGitConfiguredAsync({
nonInteractive,
}: {
nonInteractive: boolean;
}): Promise<void> {
let usernameConfigured = true;
let emailConfigured = true;
try {
await spawnAsync('git', ['config', '--local', 'user.name', username]);
spinner.succeed();
await spawnAsync('git', ['config', '--get', 'user.name'], {
cwd: this.maybeCwdOverride,
});
} catch (err: any) {
spinner.fail();
throw err;
Log.debug(err);
usernameConfigured = false;
}
}
if (!emailConfigured) {
const { email } = await promptAsync({
type: 'text',
name: 'email',
message: 'Email address:',
validate: (input: string) => input !== '',
});
const spinner = ora(`Running ${chalk.bold(`git config --local user.email ${email}`)}`).start();
try {
await spawnAsync('git', ['config', '--local', 'user.email', email]);
spinner.succeed();
await spawnAsync('git', ['config', '--get', 'user.email'], {
cwd: this.maybeCwdOverride,
});
} catch (err: any) {
spinner.fail();
throw err;
Log.debug(err);
emailConfigured = false;
}
if (usernameConfigured && emailConfigured) {
return;
}
Log.warn(
`You need to configure Git with your ${[
!usernameConfigured && 'username (user.name)',
!emailConfigured && 'email address (user.email)',
]
.filter(i => i)
.join(' and ')}`
);
if (nonInteractive) {
throw new Error('Git cannot be configured automatically in non-interactive mode');
}
if (!usernameConfigured) {
const { username } = await promptAsync({
type: 'text',
name: 'username',
message: 'Username:',
validate: (input: string) => input !== '',
});
const spinner = ora(
`Running ${chalk.bold(`git config --local user.name ${username}`)}`
).start();
try {
await spawnAsync('git', ['config', '--local', 'user.name', username], {
cwd: this.maybeCwdOverride,
});
spinner.succeed();
} catch (err: any) {
spinner.fail();
throw err;
}
}
if (!emailConfigured) {
const { email } = await promptAsync({
type: 'text',
name: 'email',
message: 'Email address:',
validate: (input: string) => input !== '',
});
const spinner = ora(
`Running ${chalk.bold(`git config --local user.email ${email}`)}`
).start();
try {
await spawnAsync('git', ['config', '--local', 'user.email', email], {
cwd: this.maybeCwdOverride,
});
spinner.succeed();
} catch (err: any) {
spinner.fail();
throw err;
}
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "16.3.3",
"version": "16.13.2",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
+5
View File
@@ -604,6 +604,11 @@
"type": "string",
"description": "The Bundle identifier that will be used when accessing submit credentials managed by Expo, it does not have any effect if you are using local credentials. In most cases this value will be autodetected, but if you have multiple Xcode schemes and targets, this value might be necessary."
},
"groups": {
"type": "array",
"description": "Internal TestFlight testing groups to add the build to (iOS only). Learn more: https://developer.apple.com/help/app-store-connect/test-a-beta-version/add-internal-testers",
"markdownDescription": "Internal TestFlight testing groups to add the build to (iOS only). [Learn more](https://developer.apple.com/help/app-store-connect/test-a-beta-version/add-internal-testers)"
},
"metadataPath": {
"type": "string",
"description": "The path to your store configuration file. Learn more: https://docs.expo.dev/eas-metadata/introduction/",
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff. Show more