Compare commits

...
43 Commits
Author SHA1 Message Date
Expo CI 10ec54345a v16.23.0 2025-10-14 15:53:39 +00:00
Kadi Kraman 27b4710264 [eas-cli] [eas-json] support custom environment variable environments (#3220) 2025-10-14 13:56:42 +01:00
Kadi Kraman 4132e92da5 Add tests for eas env:push --force (#3223) 2025-10-14 11:31:36 +01:00
Giorgio FellipeandKadi Kraman 264340a717 Add --force parameter to env:push (#3135)
Co-authored-by: Kadi Kraman <[email protected]>
2025-10-14 10:40:22 +01:00
Expo CI 1d09c2c97d update CHANGELOG.md 2025-10-13 20:55:14 +00:00
Expo CI 72cff1df1a v16.22.0 2025-10-13 20:47:55 +00:00
Douglas Lowder 6912e453ef Improvements to workflow:create (#3194)
# Why

The existing `workflow:create` command simply creates a very bare-bones workflow, that is not very useful to developers who need to know how to fully use workflows for managing builds, updates, and other EAS operations.

# How

- Define four different workflow templates, initialized as JSON objects.
- After the user selects which type of workflow (build, update, deploy, custom), sanity checks are made, EAS build and EAS update are configured if needed, and any missing build profiles are created in `eas.json`.
- Once the user has made their selections, the final workflow YAML is dynamically generated and written to disk with an explanatory header.
- After writing the workflow, next steps are provided for any further configuration that may be needed, especially if build credentials or app store credentials are needed.

In future, we can easily add more templates that use other workflow job types (fingerprint, repack, etc.).

# Test Plan

- Tested against different projects
- CI should pass
2025-10-13 13:42:00 -07:00
Expo CI 839cbac47b update CHANGELOG.md 2025-10-13 15:45:46 +00:00
Expo CI 5ba16dbc67 v16.21.0 2025-10-13 15:31:06 +00:00
Phil Pluckthun 896b456dd3 [eas-cli] Pass _expo/.routes.json for SSG exports as routes.json to Hosting deployments (#3211)
* Derive `routes.json` payload from static export `_expo/.routes.json`

* Add CHANGELOG entry
2025-10-10 15:22:32 +01:00
Kadi Kraman f2c0202f06 Add tests for the env commands (#3217) 2025-10-09 15:18:43 +01:00
Expo CI 497bac1cbf update CHANGELOG.md 2025-10-08 05:36:29 +00:00
Expo CI 5b9734b60b v16.20.4 2025-10-08 05:25:57 +00:00
Kadi Kraman 3ac70d8977 Switch to lowercase in exec and pull (#3216) 2025-10-08 06:23:06 +01:00
Douglas Lowder 32adc788d0 Support workflow:run from a Git ref (#3203)
# Why

Add a `--ref` flag so that `eas workflow:run` can initiate a workflow run from an existing ref in the project's GitHub repository.

- Avoids the need to upload the entire project from a local machine
- Ensures that the workflow starts from a known reference point in the project's source control, with no unexpected local changes

# How

- If the ref flag is set, the run command will ensure that the ref is valid, and that the workflow can be used for that ref (using the new mutation for this from https://github.com/expo/universe/pull/22132)
- If the ref flag is set, the run command starts the run from that ref (using https://github.com/expo/universe/pull/20161)
- If the user chooses ref = HEAD, `git rev-parse` is called to convert that to a commit
- The flow is designed to work the same for running from GitHub ref as running from local, so inputs and waiting for results are supported 

# Test Plan

- Tested in an existing project with workflows with and without inputs
- CI should pass
2025-10-07 15:06:24 -07:00
Expo CI e465ed57cb update CHANGELOG.md 2025-10-07 20:04:20 +00:00
Expo CI df819fc232 v16.20.3 2025-10-07 18:42:28 +00:00
Kadi Kraman 539e201972 Lower instead of uppercase environment (#3213) 2025-10-07 19:39:06 +01:00
Expo CI c14b3e179b update CHANGELOG.md 2025-10-07 15:23:38 +00:00
Expo CI f093197dea v16.20.2 2025-10-07 13:29:54 +00:00
Kadi Kraman 13834039a8 [eas-cli] Use local EnvironmentVariableEnvironment enum (#3208) 2025-10-07 14:18:07 +01:00
Expo CI 4d3023f7f1 update CHANGELOG.md 2025-10-03 15:51:00 +00:00
Expo CI eb7d3b5ecd v16.20.1 2025-10-03 15:14:03 +00:00
Stanisław Chmiela 8c721759bd [chore] Remove FingerprintSource (#3200)
<!-- 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

In https://github.com/expo/eas-build/pull/604 I removed `fingerprintSource` from metadata passed to a build (1). It was previously used to inform the build process of, well, the fingerprint source so it can optionally diff current fingerprint and fingerprint calculated by the `eas build` command.

In `eas build`, the fingerprint would get calculated and uploaded. Then it was included in the build mutation as `{ type: GCS, bucketKey }` (2). Worker expected that somewhere in the process of starting a job, something (`www` or `turtle-v2`) would swap out the persisted `fingerprintSource` to `{ type: URL, url }` (because worker can't download arbitrary GCS objects).

In https://github.com/expo/eas-build/pull/587 I changed the flow so that now worker does not expect any changes to the job or metadata and instead fetches the fingerprint sources straight from `www`.

This made `fingerprintSource` in `metadata` obsolete (see (1)). However, we still need to be able to send fingerprint sources information to `www` (see (2)).

# How

This pull request:
- bumps `@expo/eas-build-job` (removing `FingerprintSource` type)
- `FingerprintSource` gets replaced with `FingerprintSourceInput` generated by GraphQL schema
- this lets us remove `transformFingerprintSource` (it's being passed around in its final form)

# Test Plan

Set up my staging test app to use runtime version policy = fingerprint, installed and configured `expo-updates`, [ran a build](https://staging.expo.dev/accounts/sjchmiela/projects/staging-app/builds/7a44f4c3-e448-4657-a8f3-71e24a2a29c8).

I had a few `console.log`s in place:

<img width="943" height="779" alt="Zrzut ekranu 2025-09-30 o 17 39 14" src="https://github.com/user-attachments/assets/2b117a17-8c0a-4445-bf56-27f93a590f87" />

and the build has fingerprint:

<img width="1256" height="161" alt="Zrzut ekranu 2025-09-30 o 17 39 33" src="https://github.com/user-attachments/assets/b6133402-9910-42d2-b351-836ae9c10c95" />

Before I installed node dependencies, fingerprint was being set to `file:fingerprint` (probably from https://github.com/expo/expo/pull/26901, see https://github.com/expo/eas-cli/blob/b447194c8521270ba8b551296c4329c3edb7aa1c/packages/eas-cli/src/project/resolveRuntimeVersionAsync.ts#L88).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Replaces internal FingerprintSource with GraphQL FingerprintSourceInput across build/update flows, removes transformFingerprintSource, adjusts metadata wiring, and bumps related packages.
> 
> - **Build & GraphQL**:
>   - Replace `Metadata.fingerprintSource` usage with GraphQL `FingerprintSourceInput`; update builders to accept `BuildMetadataInput` and pass pre-transformed metadata.
>   - Remove `transformFingerprintSource` and associated imports/usages; simplify `transformMetadata` to accept `{ metadata, fingerprintSource }`.
>   - Update types for fingerprint source across build paths (`computeAndMaybeUpload*`, `sendBuildRequestAsync`, Android/iOS build senders).
> - **Update/Publish**:
>   - Stop transforming fingerprint source in `update publish`; pass through GraphQL `FingerprintSourceInput`.
>   - Adjust runtime fingerprint computation/assembly to use `FingerprintSourceInput`.
> - **Fingerprint upload**:
>   - `maybeUploadFingerprintAsync` now returns `FingerprintSourceInput`; use GraphQL enum casing (`Gcs`).
>   - Mutation `createFingerprintAsync` accepts `FingerprintSourceInput`.
> - **Dependencies**:
>   - Bump `@expo/eas-build-job` and `@expo/steps`; update yarn.lock accordingly.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5a61fdee68f3b0bbda043b8f3160c4c2f1ea3a71. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2025-10-03 13:06:42 +02:00
Vojtech Novak c6a741bb8c [eas-cli] skip syncing capabilities if they don't contain 'enabled' attribute (#3201) 2025-10-01 23:56:44 +02:00
Expo CI af5b76fe9f update CHANGELOG.md 2025-09-30 15:55:00 +00:00
Expo CI aa428b7571 v16.20.0 2025-09-30 15:39:44 +00:00
Colin MacKenzie ba68f40922 [eas-cli] Create new command to configure a new project (#3196)
* incorporate pr feedback

* playing around with adding build

* create expo project

* clean up getAccountChoices

* update unit test

* removed some unused code

* wip

* working on test files

* fix previews, add more end comments

* small tweaks to tests

* update changelog
2025-09-30 10:32:12 -05:00
Stanisław Chmiela 1380b023ba [eas-cli] Deprecate isGlobal GraphQL input property of environment variables (#3190)
<!-- 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

See https://github.com/expo/universe/pull/22108.

# How

- Updated GraphQL code.
- Adjusted code to the updated GraphQL code (no `isGlobal`).
- Updated types in `EnvironmentVariableMutation` so they don't re-implement what GraphQL is generating for us.
- Updated types in `env:push`.

# Test Plan

Adjusted tests.
2025-09-25 18:42:44 +02:00
Colin MacKenzie adfc79abd4 [eas-cli] Scaffolding out the new command (#3177)
* scaffolding out new command

* remove a newline

* incorporate pr feedback

* add unit test for package manager functions

* get rid of unnecessary lockfile git add

* remove unused test
2025-09-23 10:36:23 -05:00
Douglas Lowder 92e6ecd03c Improvements to workflow:validate (#3165)
# Why

Improve the validation of workflow files:

- Add client-side checks for valid syntax, and compatibility with the user's EAS build profiles
- Make validation usable with future improvements to `workflow:create`

# How

- New `validation.ts` in `commandUtils` with the various validation checks separated cleanly into different functions
- `workflow:validate` command refactored to use the new utility and wrap errors
- The new code reads the workflow schema from `<ExpoAPIBaseURL>/v2/workflows/schema`
- For testing purposes only, a local copy of the schema can be used by setting EXPO_TESTING_WORKFLOW_SCHEMA_PATH
- An existing check in `workflowFile.ts` has been modified to remove the requirement for a Github repository, as that is no longer required to run workflows

# Test Plan

- Tested against different workflow templates, and against workflows in a real project.
- Tested against the www server from the main branch running locally, to verify recent server side changes (e.g. https://github.com/expo/universe/pull/21950)
2025-09-19 02:45:58 -07:00
Expo CI 05546f0bfd update CHANGELOG.md 2025-09-17 06:43:24 +00:00
Expo CI 6c5c99b5d1 v16.19.3 2025-09-17 06:36:33 +00:00
Brent Vatne f27ea08839 [cli] Temporarily remove warnings when no compatible build found, need to refine the message (#3182) 2025-09-16 23:21:34 -07:00
Expo CI 15f47aea81 update CHANGELOG.md 2025-09-15 19:24:30 +00:00
Expo CI fd23f031b6 v16.19.2 2025-09-15 19:16:48 +00:00
Douglas Lowder a74c434718 Add artifact URLs to workflow:view (#3175)
# Why

To allow CLI users to get artifacts of builds or workflow jobs directly using the CLI, and make the URLs available in JSON output also (for easier use by automation scripts).

# How

- Add artifact info to WorkflowJobFragment
- Add this info to the results of `workflow:view`
- For workflow jobs that are of type=BUILD, use the existing build query to get the build artifact URLs for the build and show those as well
- Make some improvements to the formatting of workflow job outputs

# Test Plan

- Tested against different EAS projects
- CI should pass
2025-09-15 11:42:09 -07:00
Expo CI 29950a7629 update CHANGELOG.md 2025-09-12 11:54:08 +00:00
Expo CI 0fd33a95f5 v16.19.1 2025-09-12 11:48:48 +00:00
Stanisław Chmiela 843ed0d0aa [eas-cli] Upgrade Zod to v4 (#3173)
<!-- 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

Unify packages versions across the Expo package ecosystem (Zod, eas-build-job)

# How

Copied Zod definitions from `www` to `run.ts` where we are parsing workflow to get inputs definitions.

# Test Plan

There are, fortunately, tests for this part of the code.
2025-09-12 13:38:49 +02:00
Expo CI bf2cde45c6 update CHANGELOG.md 2025-09-12 10:35:59 +00:00
Expo CI ecab803cfa v16.19.0 2025-09-12 10:29:03 +00:00
Stanisław Chmiela 570ff126a0 [eas-cli] Allow freeform Android release track (#3163)
<!-- 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/C011ZMXQ6SZ/p1755530483378679](https://exponent-internal.slack.com/archives/C011ZMXQ6SZ/p1755530483378679)

# How

Updated GraphQL code which removed `SubmissionAndroidTrack` enum (it's a scalar now). Adjusted code to pass track around as string.

Removed `AndroidReleaseTrack` from `eas-json`. Updated `eas.schema.json` to allow any string as `track`.

Technically, to support `track: string` we'll need to update `eas-json` in `www`. I think it's ok to merge this, publish releases and then update `eas-json` in `www`. Alternatively we could publish `eas-json` from this branch as prerelease, bump in `www` and then merge this. This would be more _correct_, but I think it's ok to have this 1 hour of unannounced discrepancy between `www` and `eas-cli` (users not being able to use custom tracks _yet_). 

# Test Plan


Tested along with all the pull requests in the mix.

<img width="372" height="90" alt="Zrzut ekranu 2025-09-9 o 17 03 38" src="https://github.com/user-attachments/assets/0b260ad2-2379-4597-aa1c-dae8bf86446d" />
<img width="243" height="72" alt="Zrzut ekranu 2025-09-9 o 16 59 37" src="https://github.com/user-attachments/assets/4eef353d-ac7e-4b16-9413-a304a574b685" />
<img width="449" height="110" alt="Zrzut ekranu 2025-09-9 o 17 00 40" src="https://github.com/user-attachments/assets/58a44b7a-16bb-46b4-8d98-f233c196406a" />
<img width="366" height="85" alt="Zrzut ekranu 2025-09-9 o 17 00 35" src="https://github.com/user-attachments/assets/71013cd6-5f5e-4e38-951c-008642fbee8c" />
2025-09-12 12:08:56 +02:00
95 changed files with 5564 additions and 1404 deletions

No files matched your search

+76 -2
View File
@@ -7,16 +7,90 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
- Support custom environment variable environments ([#3220](https://github.com/expo/eas-cli/pull/3220) by [@kadikraman](https://github.com/kadikraman))
- Add --force flag to eas env:push command to bypass confirmation prompts. ([#3135](https://github.com/expo/eas-cli/pull/3135) by [@giorgiofellipe](https://github.com/giorgiofellipe))
### 🐛 Bug fixes
### 🧹 Chores
## [16.22.0](https://github.com/expo/eas-cli/releases/tag/v16.22.0) - 2025-10-13
### 🎉 New features
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
## [16.21.0](https://github.com/expo/eas-cli/releases/tag/v16.21.0) - 2025-10-13
### 🎉 New features
- Support `_expo/.routes.json` metadata file exported by `@expo/cli` for headers configuration on static deployments ([#3211](https://github.com/expo/eas-cli/pull/3211) by [@kitten](https://github.com/kitten))
### 🧹 Chores
- Add thorough tests for the env commands ([#3217](https://github.com/expo/eas-cli/pull/3217) by [@kadikraman](https://github.com/kadikraman))
## [16.20.4](https://github.com/expo/eas-cli/releases/tag/v16.20.4) - 2025-10-08
### 🎉 New features
- Allow workflow:run to run a workflow from a GitHub ref. ([#3203](https://github.com/expo/eas-cli/pull/3203) by [@douglowder](https://github.com/douglowder))
### 🐛 Bug fixes
- Fix --environment flag in eas exec and pull ([#3216](https://github.com/expo/eas-cli/pull/3216) by [@kadikraman](https://github.com/kadikraman))
## [16.20.3](https://github.com/expo/eas-cli/releases/tag/v16.20.3) - 2025-10-07
### 🐛 Bug fixes
- Fix --environment flag in eas env commands ([#3213](https://github.com/expo/eas-cli/pull/3213) by [@kadikraman](https://github.com/kadikraman))
## [16.20.2](https://github.com/expo/eas-cli/releases/tag/v16.20.2) - 2025-10-07
### 🧹 Chores
- Create an `EnvironmentVariableEnvironment` enum and use it instead of the graphQL Enum ([#3208](https://github.com/expo/eas-cli/pull/3208) by [@kadikraman](https://github.com/kadikraman))
## [16.20.1](https://github.com/expo/eas-cli/releases/tag/v16.20.1) - 2025-10-03
### 🐛 Bug fixes
- skip syncing capabilities with settings if they don't contain the `enabled` attribute ([#3201](https://github.com/expo/eas-cli/pull/3201) by [@vonovak](https://github.com/vonovak))
## [16.20.0](https://github.com/expo/eas-cli/releases/tag/v16.20.0) - 2025-09-30
### 🎉 New features
- Improvements to workflow:validate. ([#3165](https://github.com/expo/eas-cli/pull/3165) by [@douglowder](https://github.com/douglowder))
- Add `eas:new` command behind `hidden` flag. ([#3196](https://github.com/expo/eas-cli/pull/3196) by [@mackenco](https://github.com/mackenco))
## [16.19.3](https://github.com/expo/eas-cli/releases/tag/v16.19.3) - 2025-09-17
### 🧹 Chores
- Temporarily remove warnings when no compatible build found, need to refine the message. ([#3182](https://github.com/expo/eas-cli/pull/3182) by [@brentvatne](https://github.com/brentvatne))
## [16.19.2](https://github.com/expo/eas-cli/releases/tag/v16.19.2) - 2025-09-15
### 🎉 New features
- Add artifact URLs to workflow:view. ([#3175](https://github.com/expo/eas-cli/pull/3175) by [@douglowder](https://github.com/douglowder))
## [16.19.1](https://github.com/expo/eas-cli/releases/tag/v16.19.1) - 2025-09-12
## [16.19.0](https://github.com/expo/eas-cli/releases/tag/v16.19.0) - 2025-09-12
### 🎉 New features
- Allow any string as Android release `track`, not just the `production`, `internal`, `alpha`, `beta`. ([#3163](https://github.com/expo/eas-cli/pull/3163) by [@sjchmiela](https://github.com/sjchmiela))
## [16.18.1](https://github.com/expo/eas-cli/releases/tag/v16.18.1) - 2025-09-10
### 🎉 New features
- CLI debug log should respect DEBUG=`eas:*`. ([#3162](https://github.com/expo/eas-cli/pull/3162) by [@douglowder](https://github.com/douglowder))
- CLI debug log should respect DEBUG=`eas:*`. ([#3162](https://github.com/expo/eas-cli/pull/3162) by [@douglowder](https://github.com/douglowder))
- Better description for update:rollback. ([#3154](https://github.com/expo/eas-cli/pull/3154) by [@douglowder](https://github.com/douglowder))
## [16.18.0](https://github.com/expo/eas-cli/releases/tag/v16.18.0) - 2025-08-27
@@ -27,7 +101,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Make EXPO_PUBLIC_ env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman))
- Make `EXPO_PUBLIC_` env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman))
## [16.17.4](https://github.com/expo/eas-cli/releases/tag/v16.17.4) - 2025-07-31
+14 -5
View File
@@ -1,12 +1,21 @@
# Development
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
## Getting Started
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
If this is your first time running `eas-cli` locally, make sure you install and build the packages. In the project root, run:
```sh
yarn install
yarn build
```
From there, set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
> The only exception is if there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
Open your `.zshrc` or other config file and add:
```
```sh
alias easd="/PATH/TO/eas-cli/bin/run"
```
@@ -14,7 +23,7 @@ Then use it with `easd` like `easd build`.
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
```
```sh
yarn start
```
@@ -33,7 +42,7 @@ If it is easier for you to work with the code locally with compiler options set
you can consider using:
```
```sh
yarn start-allow-unused
```
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "16.18.1",
"version": "16.23.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+219 -210
View File
File diff suppressed because it is too large. Load diff
+354 -144
View File
@@ -1470,6 +1470,30 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentVariableEnvironments",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentVariables",
"description": "Environment variables for an account",
@@ -1478,7 +1502,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -1535,7 +1559,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -7114,7 +7138,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -8100,8 +8124,8 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SubmissionAndroidTrack",
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
@@ -8132,18 +8156,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "changelog",
"description": null,
@@ -8235,7 +8247,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "SubmissionAndroidTrack",
"ofType": null
}
@@ -8995,6 +9007,30 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentVariableEnvironments",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environmentVariables",
"description": "Environment variables for an app",
@@ -9003,7 +9039,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -9060,7 +9096,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -20030,7 +20066,7 @@
"description": null,
"args": [],
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -20222,6 +20258,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "waiverType",
"description": null,
"args": [],
"type": {
"kind": "ENUM",
"name": "EASBuildWaiverType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workerStartedAt",
"description": null,
@@ -20233,6 +20281,18 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workflowJob",
"description": null,
"args": [],
"type": {
"kind": "OBJECT",
"name": "WorkflowJob",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -24349,18 +24409,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "config",
"description": null,
@@ -24504,7 +24552,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
@@ -24743,7 +24791,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -25115,18 +25163,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "config",
"description": null,
@@ -25235,7 +25271,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
@@ -25257,18 +25293,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isGlobal",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
@@ -28953,7 +28977,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
@@ -29038,7 +29062,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
@@ -29155,32 +29179,13 @@
"possibleTypes": null
},
{
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"description": null,
"description": "A name of an environment variable environment",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "DEVELOPMENT",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PREVIEW",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PRODUCTION",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"enumValues": null,
"possibleTypes": null
},
{
@@ -29694,7 +29699,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
@@ -29783,7 +29788,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
@@ -30107,6 +30112,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "platformBreakdown",
"description": null,
"args": [],
"type": {
"kind": "OBJECT",
"name": "EstimatedUsagePlatformBreakdown",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "service",
"description": null,
@@ -30161,6 +30178,92 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EstimatedUsagePlatformBreakdown",
"description": null,
"fields": [
{
"name": "android",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EstimatedUsagePlatformDetail",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ios",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EstimatedUsagePlatformDetail",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EstimatedUsagePlatformDetail",
"description": null,
"fields": [
{
"name": "limit",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "Experiment",
@@ -32072,7 +32175,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -32203,7 +32306,7 @@
"description": null,
"args": [],
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -36305,7 +36408,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -37045,18 +37148,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archiveUrl",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ascApiKey",
"description": null,
@@ -41619,7 +41710,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -48506,6 +48597,18 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workflowJob",
"description": null,
"args": [],
"type": {
"kind": "OBJECT",
"name": "WorkflowJob",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -48578,38 +48681,13 @@
"possibleTypes": null
},
{
"kind": "ENUM",
"kind": "SCALAR",
"name": "SubmissionAndroidTrack",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "ALPHA",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "BETA",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INTERNAL",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PRODUCTION",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"enumValues": null,
"possibleTypes": null
},
{
@@ -49858,7 +49936,7 @@
"description": null,
"args": [],
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -50136,6 +50214,18 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workflowJob",
"description": null,
"args": [],
"type": {
"kind": "OBJECT",
"name": "WorkflowJob",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -51508,7 +51598,7 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
@@ -51546,18 +51636,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isGlobal",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
@@ -51700,7 +51778,7 @@
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"kind": "SCALAR",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
@@ -63306,6 +63384,12 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "GITHUB_COMMENT",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "MAESTRO_CLOUD",
"description": null,
@@ -63740,6 +63824,71 @@
"name": "WorkflowRevisionMutation",
"description": null,
"fields": [
{
"name": "getOrCreateWorkflowRevisionFromGitRef",
"description": null,
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fileName",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitRef",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "WorkflowRevision",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "validateWorkflowYamlConfig",
"description": null,
@@ -64533,6 +64682,67 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createWorkflowRunFromGitRef",
"description": null,
"args": [
{
"name": "gitRef",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "inputs",
"description": null,
"type": {
"kind": "SCALAR",
"name": "JSONObject",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workflowRevisionId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "WorkflowRun",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "retryWorkflowRun",
"description": null,
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "16.18.1",
"version": "16.23.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -12,11 +12,11 @@
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.173",
"@expo/eas-json": "16.18.0",
"@expo/eas-build-job": "1.0.237",
"@expo/eas-json": "16.23.0",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.117",
"@expo/logger": "1.0.221",
"@expo/multipart-body-parser": "2.0.0",
"@expo/osascript": "2.1.4",
"@expo/package-manager": "1.7.0",
@@ -28,7 +28,7 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.173",
"@expo/steps": "1.0.231",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
@@ -85,13 +85,14 @@
"tar": "6.2.1",
"tar-stream": "3.1.7",
"terminal-link": "2.1.1",
"ts-deepmerge": "6.2.0",
"tslib": "2.6.2",
"turndown": "7.1.2",
"untildify": "4.0.0",
"uuid": "9.0.1",
"wrap-ansi": "7.0.0",
"yaml": "2.6.0",
"zod": "^3.23.8"
"zod": "^4.1.3"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
@@ -126,7 +127,6 @@
"mockdate": "3.0.5",
"nock": "13.4.0",
"rimraf": "3.0.2",
"ts-deepmerge": "6.2.0",
"ts-mockito": "2.6.1",
"ts-node": "10.9.2",
"typescript": "5.3.3"
@@ -270,6 +270,7 @@ export function getMockWorkflowJobFragment(
id: 'job1',
logFileUrls: ['https://example.com/log1'],
errors: [],
artifacts: [],
},
};
}
+4
View File
@@ -108,3 +108,7 @@ export function getEASUpdateURL(projectId: string, manifestHostOverride: string
return new URL(projectId, `https://u.expo.dev`).href;
}
}
export function getExpoApiWorkflowSchemaURL(): string {
return getExpoApiBaseUrl() + '/v2/workflows/schema';
}
+3 -5
View File
@@ -1,4 +1,4 @@
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { Android, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import getenv from 'getenv';
@@ -11,7 +11,7 @@ import { resolveRemoteVersionCodeAsync } from './version';
import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMetadataInput, BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log, { learnMore } from '../../log';
import {
@@ -27,7 +27,6 @@ import {
prepareBuildRequestForPlatformAsync,
} from '../build';
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials';
import {
checkGoogleServicesFileAsync,
@@ -124,10 +123,9 @@ export async function prepareAndroidBuildAsync(
sendBuildRequestAsync: async (
appId: string,
job: Android.Job,
metadata: Metadata,
graphqlMetadata: BuildMetadataInput,
buildParams: BuildParamsInput
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
appId,
@@ -8,7 +8,6 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -30,7 +29,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
environment: job.environment,
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
+17 -9
View File
@@ -2,7 +2,6 @@ import {
ArchiveSource,
ArchiveSourceType,
BuildJob,
FingerprintSource,
Metadata,
Platform,
} from '@expo/eas-build-job';
@@ -50,9 +49,11 @@ import { createFingerprintAsync } from '../fingerprint/cli';
import {
AppPlatform,
BuildFragment,
BuildMetadataInput,
BuildParamsInput,
BuildPriority,
BuildStatus,
FingerprintSourceInput,
UploadSessionType,
} from '../graphql/generated';
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
@@ -94,7 +95,7 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob
sendBuildRequestAsync(
appId: string,
job: TJob,
metadata: Metadata,
metadata: BuildMetadataInput,
buildParams: BuildParamsInput
): Promise<BuildResult>;
}
@@ -193,13 +194,20 @@ export async function prepareBuildRequestForPlatformAsync<
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
buildId: nullthrows(process.env.EAS_BUILD_ID),
metadata: transformMetadata(metadata),
metadata: transformMetadata({
metadata,
fingerprintSource: runtimeAndFingerprintMetadata.fingerprintSource ?? undefined,
}),
});
printJsonOnlyOutput({ job, metadata });
return undefined;
} else if (!ctx.localBuildOptions.localBuildMode) {
try {
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
const graphqlMetadata = transformMetadata({
metadata,
fingerprintSource: runtimeAndFingerprintMetadata.fingerprintSource ?? undefined,
});
return await sendBuildRequestAsync(builder, job, graphqlMetadata, buildParams);
} catch (error: any) {
handleBuildRequestError(error, job.platform);
}
@@ -362,7 +370,7 @@ async function sendBuildRequestAsync<
>(
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata,
graphqlMetadata: BuildMetadataInput,
buildParams: BuildParamsInput
): Promise<BuildFragment> {
const { ctx } = builder;
@@ -376,7 +384,7 @@ async function sendBuildRequestAsync<
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.projectId,
job,
metadata,
graphqlMetadata,
buildParams
);
@@ -654,7 +662,7 @@ async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends
): Promise<{
runtimeVersion?: string | undefined;
fingerprintHash?: string | undefined;
fingerprintSource?: FingerprintSource | undefined;
fingerprintSource?: FingerprintSourceInput | undefined;
}> {
const runtimeAndFingerprintMetadata =
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
@@ -676,7 +684,7 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
ctx: BuildContext<T>
): Promise<{
runtimeVersion?: string;
fingerprintSource?: FingerprintSource;
fingerprintSource?: FingerprintSourceInput;
fingerprintHash?: string;
}> {
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
@@ -723,7 +731,7 @@ async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends
{ debug }: { debug?: boolean } = {}
): Promise<{
fingerprintHash?: string;
fingerprintSource?: FingerprintSource;
fingerprintSource?: FingerprintSourceInput;
}> {
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
workflow: ctx.workflow,
@@ -1,9 +1,8 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { isEnvironment } from './utils/environment';
import { DefaultEnvironment } from './utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log, { learnMore } from '../log';
@@ -49,20 +48,9 @@ async function resolveEnvVarsAsync({
projectId: string;
}): Promise<Env> {
const environment =
buildProfile.environment?.toUpperCase() ??
buildProfile.environment ??
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
if (!isEnvironment(environment)) {
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
buildProfile.env && Object.keys(buildProfile.env).length > 0
? Object.keys(buildProfile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
return { ...buildProfile.env };
}
try {
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
@@ -129,16 +117,16 @@ async function resolveEnvVarsAsync({
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
buildProfile: BuildProfile
): EnvironmentVariableEnvironment {
): DefaultEnvironment {
const environment =
buildProfile.distribution === 'store'
? EnvironmentVariableEnvironment.Production
? DefaultEnvironment.Production
: buildProfile.developmentClient
? EnvironmentVariableEnvironment.Development
: EnvironmentVariableEnvironment.Preview;
? DefaultEnvironment.Development
: DefaultEnvironment.Preview;
Log.log(
`Resolved "${environment.toLowerCase()}" environment for the build. ${learnMore(
`Resolved "${environment}" environment for the build. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
)}`
);
+8 -19
View File
@@ -3,7 +3,6 @@ import {
ArchiveSourceType,
BuildMode,
BuildTrigger,
FingerprintSourceType,
Metadata,
Workflow,
} from '@expo/eas-build-job';
@@ -19,7 +18,6 @@ import {
FingerprintSourceInput,
BuildMode as GraphQLBuildMode,
BuildTrigger as GraphQLBuildTrigger,
FingerprintSourceType as GraphQLFingeprintSourceType,
ProjectArchiveSourceInput,
ProjectArchiveSourceType,
WorkerLoggerLevel,
@@ -42,11 +40,16 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
}
}
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
export function transformMetadata({
metadata,
fingerprintSource,
}: {
metadata: Metadata;
fingerprintSource: FingerprintSourceInput | undefined;
}): BuildMetadataInput {
return {
...metadata,
fingerprintSource:
metadata.fingerprintSource && transformFingerprintSource(metadata.fingerprintSource),
fingerprintSource,
credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
@@ -57,20 +60,6 @@ export function transformMetadata(metadata: Metadata): BuildMetadataInput {
};
}
export function transformFingerprintSource(
fingerprintSource: NonNullable<Metadata['fingerprintSource']>
): FingerprintSourceInput | null {
if (fingerprintSource.type !== FingerprintSourceType.GCS) {
return null;
}
return {
type: GraphQLFingeprintSourceType.Gcs,
bucketKey: fingerprintSource.bucketKey,
isDebugFingerprint: fingerprintSource.isDebugFingerprint,
};
}
function transformCredentialsSource(
credentialsSource: Metadata['credentialsSource']
): BuildCredentialsSource {
+3 -5
View File
@@ -1,4 +1,4 @@
import { Ios, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { Ios, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import { ensureIosCredentialsAsync } from './credentials';
@@ -7,7 +7,7 @@ import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteBuildNumberAsync } from './version';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMetadataInput, BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
@@ -15,7 +15,6 @@ import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommonContext, IosBuildContext } from '../context';
import { transformMetadata } from '../graphql';
import {
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
@@ -106,10 +105,9 @@ export async function prepareIosBuildAsync(
sendBuildRequestAsync: async (
appId: string,
job: Ios.Job,
metadata: Metadata,
graphqlMetadata: BuildMetadataInput,
buildParams: BuildParamsInput
): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job);
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
appId,
+1 -2
View File
@@ -9,7 +9,6 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -32,7 +31,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
environment: job.environment,
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
+1 -3
View File
@@ -1,4 +1,4 @@
import { FingerprintSource, Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
@@ -22,7 +22,6 @@ export async function collectMetadataAsync<T extends Platform>(
runtimeAndFingerprintMetadata: {
runtimeVersion?: string | undefined;
fingerprintHash?: string | undefined;
fingerprintSource?: FingerprintSource | undefined;
}
): Promise<Metadata> {
const channelObject = await resolveChannelAsync(ctx);
@@ -36,7 +35,6 @@ export async function collectMetadataAsync<T extends Platform>(
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: runtimeAndFingerprintMetadata?.runtimeVersion,
fingerprintHash: runtimeAndFingerprintMetadata?.fingerprintHash,
fingerprintSource: runtimeAndFingerprintMetadata?.fingerprintSource,
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
...channelObject,
distribution,
@@ -1,26 +1,5 @@
import { BuildProfile } from '@expo/eas-json';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
type Environment = NonNullable<BuildProfile['environment']>;
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
production: EnvironmentVariableEnvironment.Production,
preview: EnvironmentVariableEnvironment.Preview,
development: EnvironmentVariableEnvironment.Development,
};
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export function buildProfileEnvironmentToEnvironment(
environment: BuildProfile['environment']
): EnvironmentVariableEnvironment | null {
if (!environment) {
return null;
}
return BuildProfileEnvironmentToEnvironment[environment];
export enum DefaultEnvironment {
Development = 'development',
Preview = 'preview',
Production = 'production',
}
+37 -22
View File
@@ -8,7 +8,42 @@ import {
} from '../../graphql/generated';
import { link } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import formatFields from '../../utils/formatFields';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
export function formatGraphQLBuildArtifacts(build: BuildFragment): FormatFieldsItem[] {
const fields = [
{
label: 'Application Archive URL',
get value() {
switch (build.status) {
case GraphQLBuildStatus.InProgress:
return '<in progress>';
default: {
const url = build.artifacts?.buildUrl;
return url ? link(url) : 'null';
}
}
},
},
{
label: 'Build Artifacts URL',
get value() {
switch (build.status) {
case GraphQLBuildStatus.InProgress:
return '<in progress>';
default: {
const url = build.artifacts?.buildArtifactsUrl;
return url ? link(url) : 'null';
}
}
},
},
];
return fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
}
export function formatGraphQLBuild(build: BuildFragment): string {
const actor = getActorName(build);
@@ -84,27 +119,7 @@ export function formatGraphQLBuild(build: BuildFragment): string {
label: 'Logs',
value: link(getBuildLogsUrl(build)),
},
{
label: 'Artifact',
get value() {
switch (build.status) {
case GraphQLBuildStatus.New:
case GraphQLBuildStatus.InQueue:
case GraphQLBuildStatus.InProgress:
return '<in progress>';
case GraphQLBuildStatus.PendingCancel:
case GraphQLBuildStatus.Canceled:
case GraphQLBuildStatus.Errored:
return null;
case GraphQLBuildStatus.Finished: {
const url = build.artifacts?.buildUrl;
return url ? link(url) : chalk.red('not found');
}
default:
return null;
}
},
},
...formatGraphQLBuildArtifacts(build),
{
label: 'Fingerprint',
get value() {
@@ -26,7 +26,6 @@ import {
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import Log, { link } from '../log';
import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
@@ -60,7 +59,7 @@ interface BaseGetContextAsyncArgs {
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
extends BaseGetContextAsyncArgs {
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
withServerSideEnvironment: string | null;
}
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
@@ -1,5 +1,4 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import SessionManager from '../../user/SessionManager';
import { Client } from '../../vcs/vcs';
@@ -11,7 +10,7 @@ export interface ContextOptions {
/**
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
*/
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
withServerSideEnvironment?: string | null;
}
export default abstract class ContextField<T> {
@@ -1,23 +1,15 @@
import { ExpoGraphqlClient } from './createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
const cachedServerSideEnvironmentVariables: Record<
EnvironmentVariableEnvironment,
Record<string, string> | null
> = {
[EnvironmentVariableEnvironment.Development]: null,
[EnvironmentVariableEnvironment.Preview]: null,
[EnvironmentVariableEnvironment.Production]: null,
};
const cachedServerSideEnvironmentVariables: Record<string, Record<string, string> | undefined> = {};
export async function loadServerSideEnvironmentVariablesAsync({
environment,
projectId,
graphqlClient,
}: {
environment: EnvironmentVariableEnvironment;
environment: string;
projectId: string;
graphqlClient: ExpoGraphqlClient;
}): Promise<Record<string, string>> {
@@ -42,13 +34,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment}" environment on EAS: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment}" environment on EAS.`
);
}
Log.newLine();
+8 -26
View File
@@ -1,17 +1,5 @@
import { Flags } from '@oclif/core';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
// are used in tandem, it saves on unnecessary typying in commands
async function upperCaseAsync<T>(input: string): Promise<T> {
return input.toUpperCase() as T;
}
function mapToLowercase<T extends string>(options: T[]): T[] {
return options.map(option => option.toLowerCase()) as T[];
}
export const EasNonInteractiveAndJsonFlags = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
@@ -23,21 +11,17 @@ export const EasNonInteractiveAndJsonFlags = {
};
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
};
export const EASEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
environment: Flags.string({
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
}),
};
export const EASMultiEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>({
environment: Flags.string({
...EasEnvironmentFlagParameters,
multiple: true,
}),
@@ -81,12 +65,10 @@ export const EasJsonOnlyFlag = {
};
export const EasUpdateEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
environment: Flags.string({
description:
'Environment to use for the server-side defined EAS environment variables during command execution.',
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
parse: upperCaseAsync,
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development"',
required: false,
default: null,
default: undefined,
}),
};
@@ -0,0 +1,269 @@
import { ExpoConfig } from '@expo/config';
import { BuildProfile, EasJsonAccessor, EasJsonUtils, Platform } from '@expo/eas-json';
import { AndroidBuildProfile, IosBuildProfile } from '@expo/eas-json/build/build/types';
import BuildConfigure from '../../commands/build/configure';
import UpdateConfigure from '../../commands/update/configure';
import Log from '../../log';
import { promptAsync } from '../../prompts';
export async function buildProfileNamesFromProjectAsync(projectDir: string): Promise<Set<string>> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const buildProfileNames = new Set(
easJsonAccessor && (await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor))
);
return buildProfileNames;
}
export async function getBuildProfileAsync(
projectDir: string,
platform: Platform,
profileName: string
): Promise<BuildProfile<Platform>> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
profileName
);
return buildProfile;
}
export async function buildProfilesFromProjectAsync(
projectDir: string
): Promise<Map<string, { android: AndroidBuildProfile; ios: IosBuildProfile }>> {
const buildProfileNames = await buildProfileNamesFromProjectAsync(projectDir);
const buildProfiles: Map<string, { android: AndroidBuildProfile; ios: IosBuildProfile }> =
new Map();
for (const profileName of buildProfileNames) {
const iosBuildProfile = (await getBuildProfileAsync(
projectDir,
Platform.IOS,
profileName
)) as IosBuildProfile;
const androidBuildProfile = (await getBuildProfileAsync(
projectDir,
Platform.ANDROID,
profileName
)) as AndroidBuildProfile;
buildProfiles.set(profileName, { android: androidBuildProfile, ios: iosBuildProfile });
}
return buildProfiles;
}
export function isBuildProfileForDevelopment(
buildProfile: BuildProfile<Platform>,
platform: Platform
): boolean {
if (buildProfile.developmentClient) {
return true;
}
if (platform === Platform.ANDROID) {
return (buildProfile as BuildProfile<Platform.ANDROID>).gradleCommand === ':app:assembleDebug';
}
if (platform === Platform.IOS) {
return (buildProfile as BuildProfile<Platform.IOS>).buildConfiguration === 'Debug';
}
return false;
}
export function isIosBuildProfileForSimulator(buildProfile: BuildProfile<Platform.IOS>): boolean {
return buildProfile.simulator ?? false;
}
export async function addAndroidDevelopmentBuildProfileToEasJsonAsync(
projectDir: string,
buildProfileName: string
): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.build = {
...easJsonRawObject.build,
[buildProfileName]: {
developmentClient: true,
distribution: 'internal',
android: {
gradleCommand: ':app:assembleDebug',
},
},
};
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
}
export async function addIosDevelopmentBuildProfileToEasJsonAsync(
projectDir: string,
buildProfileName: string,
simulator: boolean
): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.build = {
...easJsonRawObject.build,
[buildProfileName]: {
developmentClient: true,
distribution: 'internal',
ios: {
buildConfiguration: 'Debug',
simulator,
},
},
};
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
}
export async function addProductionBuildProfileToEasJsonIfNeededAsync(
projectDir: string
): Promise<boolean> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
let profileAdded = false;
easJsonAccessor.patch(easJsonRawObject => {
if (!easJsonRawObject.build?.production) {
profileAdded = true;
easJsonRawObject.build = {
...(easJsonRawObject.build ?? {}),
production: {},
};
// Also add the profile to submit
easJsonRawObject.submit = {
...(easJsonRawObject.submit ?? {}),
production: {},
};
}
return easJsonRawObject;
});
if (profileAdded) {
Log.log('Added missing production build profile to eas.json');
}
await easJsonAccessor.writeAsync();
return profileAdded;
}
export async function hasBuildConfigureBeenRunAsync({
projectDir,
expoConfig,
}: {
projectDir: string;
expoConfig: ExpoConfig;
}): Promise<boolean> {
// Is there a project ID in the Expo config?
if (!expoConfig.extra?.eas?.projectId) {
return false;
}
// Is there an eas.json?
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
try {
await easJsonAccessor.readAsync();
} catch {
return false;
}
return true;
}
export async function hasUpdateConfigureBeenRunAsync({
projectDir,
expoConfig,
}: {
projectDir: string;
expoConfig: ExpoConfig;
}): Promise<boolean> {
// Does the Expo config have an updates URL?
if (!expoConfig.updates?.url) {
return false;
}
// Does at least one build profile have a channel?
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
try {
const easJson = await easJsonAccessor.readAsync();
return Object.values(easJson.build ?? {}).some(buildProfile => !!buildProfile.channel);
} catch {
return false;
}
}
/**
* Runs update:configure if needed. Returns a boolean (proceed with workflow creation, or not)
*/
export async function runUpdateConfigureIfNeededAsync({
projectDir,
expoConfig,
}: {
projectDir: string;
expoConfig: ExpoConfig;
}): Promise<boolean> {
if (
await hasUpdateConfigureBeenRunAsync({
projectDir,
expoConfig,
})
) {
return true;
}
const nextStep = (
await promptAsync({
type: 'select',
name: 'nextStep',
message:
'You have chosen to create a workflow that requires EAS Update configuration. What would you like to do?',
choices: [
{ title: 'Configure EAS Update and then proceed', value: 'configure' },
{ title: 'EAS Update is already configured, proceed', value: 'proceed' },
{ title: 'Choose a different workflow template', value: 'repeat' },
],
})
).nextStep;
switch (nextStep) {
case 'configure':
Log.newLine();
await UpdateConfigure.run([]);
return true;
case 'proceed':
return true;
default:
return false;
}
}
/**
* Runs build:configure if needed. Returns a boolean (proceed with workflow creation, or not)
*/
export async function runBuildConfigureIfNeededAsync({
projectDir,
expoConfig,
}: {
projectDir: string;
expoConfig: ExpoConfig;
}): Promise<boolean> {
if (
await hasBuildConfigureBeenRunAsync({
projectDir,
expoConfig,
})
) {
return true;
}
const nextStep = (
await promptAsync({
type: 'select',
name: 'nextStep',
message:
'You have chosen to create a workflow that requires EAS Build configuration. What would you like to do?',
choices: [
{ title: 'Configure EAS Build and then proceed', value: 'configure' },
{ title: 'EAS Build is already configured, proceed', value: 'proceed' },
{ title: 'Choose a different workflow template', value: 'repeat' },
],
})
).nextStep;
switch (nextStep) {
case 'configure':
Log.newLine();
await BuildConfigure.run(['-p', 'all']);
return true;
case 'proceed':
return true;
default:
return false;
}
}
@@ -0,0 +1,536 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import {
addAndroidDevelopmentBuildProfileToEasJsonAsync,
addIosDevelopmentBuildProfileToEasJsonAsync,
addProductionBuildProfileToEasJsonIfNeededAsync,
buildProfilesFromProjectAsync,
hasBuildConfigureBeenRunAsync,
hasUpdateConfigureBeenRunAsync,
isBuildProfileForDevelopment,
isIosBuildProfileForSimulator,
} from './buildProfileUtils';
import Log, { link } from '../../log';
import { confirmAsync } from '../../prompts';
import { easCliVersion } from '../../utils/easCli';
export enum WorkflowStarterName {
BUILD = 'build',
UPDATE = 'update',
CUSTOM = 'custom',
DEPLOY = 'deploy',
}
export type WorkflowStarter = {
name: WorkflowStarterName;
displayName: string;
defaultFileName: string;
template: any;
header: string;
nextSteps?: string[];
};
const createdByEASCLI = `# Created by EAS CLI v${easCliVersion}`;
const CUSTOM_TEMPLATE = {
name: 'Custom workflow',
on: {
push: {
branches: ['main'],
},
},
jobs: {
custom_build: {
name: 'Custom job',
steps: [
{
uses: 'eas/checkout',
},
{
name: 'Hello World',
id: 'hello_world',
run: '# Custom script\necho "Hello, World"\n',
},
],
},
},
};
const CUSTOM_TEMPLATE_HEADER = `
# Custom job
#
# This workflow shows how to write custom jobs.
# It contains a predefined workflow step and a shell command to print "Hello, World!".
#
# Key features:
# - Triggers on pushes to the main branch.
# (Requires linked GitHub account: https://expo.dev/accounts/[account]/settings/github)
# - Can be triggered manually with eas workflow:run custom.yml
# - Runs eas/checkout then a custom "echo" command
#
# For detailed documentation on workflow syntax and available step types, visit:
# https://docs.expo.dev/eas/workflows/syntax/#jobsjob_idstepsstepuses
#
${createdByEASCLI}
#
`;
const BUILD_TEMPLATE = {
name: 'Create development builds',
on: {
push: {
branches: ['main'],
},
},
jobs: {},
};
const BUILD_TEMPLATE_HEADER = `
# Create development builds
#
# This workflow shows how to create development builds.
#
# Key features:
# - Can be triggered manually with eas workflow:run create-development-builds.yml
# - Runs the pre-packaged build job to create Android and iOS development builds
# for Android emulators, Android and iOS devices, and iOS simulators
#
# For a detailed guide on using this workflow, visit:
# https://docs.expo.dev/develop/development-builds/introduction/
#
${createdByEASCLI}
#
`;
const PUBLISH_UPDATE_TEMPLATE = {
name: 'Publish preview update',
on: {
push: {
branches: ['*'],
},
},
jobs: {
publish_preview_update: {
name: 'Publish preview update',
type: 'update',
params: {
branch: '${{ github.ref_name || "test" }}',
},
},
},
};
const PUBLISH_UPDATE_TEMPLATE_HEADER = `
# Publish preview update
#
# This workflow shows how to publish preview updates.
# Learn more: https://docs.expo.dev/review/share-previews-with-your-team/
#
# Key features:
# - Triggers on pushes to all branches
# (Requires linked GitHub account: https://expo.dev/accounts/[account]/settings/github)
# - Can be triggered manually with eas workflow:run publish-preview-update.yml
# - Runs the pre-packaged update job
#
# For a detailed guide on using this workflow, visit:
# https://docs.expo.dev/eas/workflows/examples/publish-preview-update/
#
${createdByEASCLI}
#
`;
const DEPLOY_TEMPLATE = {
name: 'Deploy to production',
on: {
push: {
branches: ['main'],
},
},
jobs: {
fingerprint: {
name: 'Fingerprint',
type: 'fingerprint',
},
get_android_build: {
name: 'Check for existing android build',
needs: ['fingerprint'],
type: 'get-build',
params: {
fingerprint_hash: '${{ needs.fingerprint.outputs.android_fingerprint_hash }}',
profile: 'production',
},
},
get_ios_build: {
name: 'Check for existing ios build',
needs: ['fingerprint'],
type: 'get-build',
params: {
fingerprint_hash: '${{ needs.fingerprint.outputs.ios_fingerprint_hash }}',
profile: 'production',
},
},
build_android: {
name: 'Build Android',
needs: ['get_android_build'],
if: '${{ !needs.get_android_build.outputs.build_id }}',
type: 'build',
params: {
platform: 'android',
profile: 'production',
},
},
build_ios: {
name: 'Build iOS',
needs: ['get_ios_build'],
if: '${{ !needs.get_ios_build.outputs.build_id }}',
type: 'build',
params: {
platform: 'ios',
profile: 'production',
},
},
submit_android_build: {
name: 'Submit Android Build',
needs: ['build_android'],
type: 'submit',
params: {
build_id: '${{ needs.build_android.outputs.build_id }}',
},
},
submit_ios_build: {
name: 'Submit iOS Build',
needs: ['build_ios'],
type: 'submit',
params: {
build_id: '${{ needs.build_ios.outputs.build_id }}',
},
},
publish_android_update: {
name: 'Publish Android update',
needs: ['get_android_build'],
if: '${{ needs.get_android_build.outputs.build_id }}',
type: 'update',
params: {
branch: 'production',
platform: 'android',
},
},
publish_ios_update: {
name: 'Publish iOS update',
needs: ['get_ios_build'],
if: '${{ needs.get_ios_build.outputs.build_id }}',
type: 'update',
params: {
branch: 'production',
platform: 'ios',
},
},
},
};
const DEPLOY_TEMPLATE_HEADER = `
# Deploy to production
#
# This workflow shows how to build and submit to app stores.
#
# Key features:
# - Triggers on pushes to "main" (Requires linked GitHub account: https://expo.dev/accounts/[account]/settings/github)
# - Can be triggered manually with eas workflow:run deploy-to-production.yml
# - Creates builds and submits them to app stores when native changes are detected, otherwise sends an over-the-air update.
#
# For a detailed guide on using this workflow, visit:
# https://docs.expo.dev/eas/workflows/examples/deploy-to-production/
`;
function nextStepForDeviceBuildProfile(buildProfileName: string): string {
return `A build job in this workflow uses the build profile "${buildProfileName}"
to build your app for real devices, and may require credentials.
You can configure your credentials with the command "eas credentials".
For more information, please see ${link('https://docs.expo.dev/app-signing/app-credentials/')}.
`
.trim()
.trimStart();
}
function nextStepForAppSubmission(): string {
return `This workflow includes a job to submit your app to app stores.
You will need to configure your App Store/Play Store credentials
before you can run this workflow.
Please see ${link('https://docs.expo.dev/deploy/submit-to-app-stores/')}.
`
.trim()
.trimStart();
}
export function howToRunWorkflow(
workflowFileName: string,
workflowStarter: WorkflowStarter
): string {
const lines = [
`To run this workflow manually, use the command "eas workflow:run .eas/workflows/${workflowFileName}".`,
];
if (workflowStarter.template?.on?.push?.branches?.length > 0) {
const branches = workflowStarter.template.on.push.branches;
if (branches.length === 1 && branches[0] === '*') {
lines.push(
'This workflow is also configured to run automatically when code is pushed to any branch.'
);
} else if (branches.length === 1) {
lines.push(
`This workflow is also configured to run automatically when code is pushed to the "${branches[0]}" branch.`
);
} else {
lines.push(
`This workflow is also configured to run automatically when code is pushed to the following branches: ${branches.join(
', '
)}.`
);
}
}
return lines.join('\n');
}
export const workflowStarters: WorkflowStarter[] = [
{
displayName: 'Custom',
name: WorkflowStarterName.CUSTOM,
defaultFileName: 'custom.yml',
template: CUSTOM_TEMPLATE,
header: CUSTOM_TEMPLATE_HEADER,
},
{
displayName: 'Create development builds',
name: WorkflowStarterName.BUILD,
defaultFileName: 'create-development-builds.yml',
template: BUILD_TEMPLATE,
header: BUILD_TEMPLATE_HEADER,
},
{
displayName: 'Publish updates',
name: WorkflowStarterName.UPDATE,
defaultFileName: 'publish-updates.yml',
template: PUBLISH_UPDATE_TEMPLATE,
header: PUBLISH_UPDATE_TEMPLATE_HEADER,
},
{
displayName: 'Deploy to production',
name: WorkflowStarterName.DEPLOY,
defaultFileName: 'deploy-to-production.yml',
template: DEPLOY_TEMPLATE,
header: DEPLOY_TEMPLATE_HEADER,
},
];
export async function addBuildJobsToDevelopmentBuildTemplateAsync(
projectDir: string,
workflowStarter: WorkflowStarter
): Promise<WorkflowStarter> {
const buildProfiles = await buildProfilesFromProjectAsync(projectDir);
// android_development_build
const nextSteps: Set<string> = new Set();
let androidDevelopmentBuildProfileName: string | null = null;
for (const profileName of buildProfiles.keys()) {
const profile = buildProfiles.get(profileName)?.android;
if (!profile) {
continue;
}
if (isBuildProfileForDevelopment(profile, Platform.ANDROID)) {
androidDevelopmentBuildProfileName = profileName;
break;
}
}
if (!androidDevelopmentBuildProfileName) {
Log.warn(
'This workflow requires an Android development build profile in your eas.json, but none were found.'
);
const add = await confirmAsync({
message: 'Do you want to add an Android development build profile?',
initial: false,
});
if (add) {
let androidDevelopmentBuildProfileName = 'android_development';
while (buildProfiles.has(androidDevelopmentBuildProfileName)) {
androidDevelopmentBuildProfileName = `${androidDevelopmentBuildProfileName}_1`;
}
await addAndroidDevelopmentBuildProfileToEasJsonAsync(
projectDir,
androidDevelopmentBuildProfileName
);
} else {
Log.log('Skipping Android development build job...');
}
}
if (androidDevelopmentBuildProfileName) {
Log.log(`Using Android development build profile: ${androidDevelopmentBuildProfileName}`);
workflowStarter.template.jobs.android_development_build = {
name: `Build ${androidDevelopmentBuildProfileName} for android`,
type: 'build',
params: {
profile: androidDevelopmentBuildProfileName,
platform: 'android',
},
};
nextSteps.add(nextStepForDeviceBuildProfile(androidDevelopmentBuildProfileName));
}
// ios_simulator_development_build
let iosSimulatorDevelopmentBuildProfileName: string | null = null;
for (const profileName of buildProfiles.keys()) {
const profile = buildProfiles.get(profileName)?.ios;
if (!profile) {
continue;
}
if (
isBuildProfileForDevelopment(profile, Platform.IOS) &&
isIosBuildProfileForSimulator(profile)
) {
iosSimulatorDevelopmentBuildProfileName = profileName;
break;
}
}
if (!iosSimulatorDevelopmentBuildProfileName) {
Log.warn(
'This workflow requires an iOS simulator development build profile in your eas.json, but none were found.'
);
const add = await confirmAsync({
message: 'Do you want to add an iOS simulator development build profile?',
initial: false,
});
if (add) {
let iosSimulatorDevelopmentBuildProfileName = 'ios_simulator_development';
while (buildProfiles.has(iosSimulatorDevelopmentBuildProfileName)) {
iosSimulatorDevelopmentBuildProfileName = `${iosSimulatorDevelopmentBuildProfileName}_1`;
}
await addIosDevelopmentBuildProfileToEasJsonAsync(
projectDir,
iosSimulatorDevelopmentBuildProfileName,
true
);
} else {
Log.log('Skipping iOS simulator development build job...');
}
}
if (iosSimulatorDevelopmentBuildProfileName) {
Log.log(
`Using iOS simulator development build profile: ${iosSimulatorDevelopmentBuildProfileName}`
);
workflowStarter.template.jobs.ios_simulator_development_build = {
name: `Build ${iosSimulatorDevelopmentBuildProfileName} for iOS simulator`,
type: 'build',
params: {
profile: iosSimulatorDevelopmentBuildProfileName,
platform: 'ios',
},
};
}
// ios_device_development_build
let iosDeviceDevelopmentBuildProfileName: string | null = null;
for (const profileName of buildProfiles.keys()) {
const profile = buildProfiles.get(profileName)?.ios;
if (!profile) {
continue;
}
if (
isBuildProfileForDevelopment(profile, Platform.IOS) &&
!isIosBuildProfileForSimulator(profile)
) {
iosDeviceDevelopmentBuildProfileName = profileName;
break;
}
}
if (!iosDeviceDevelopmentBuildProfileName) {
Log.warn(
'This workflow requires an iOS device development build profile in your eas.json, but none were found.'
);
const add = await confirmAsync({
message: 'Do you want to add an iOS device development build profile?',
initial: false,
});
if (add) {
let iosDeviceDevelopmentBuildProfileName = 'ios_device_development';
while (buildProfiles.has(iosDeviceDevelopmentBuildProfileName)) {
iosDeviceDevelopmentBuildProfileName = `${iosDeviceDevelopmentBuildProfileName}_1`;
}
await addIosDevelopmentBuildProfileToEasJsonAsync(
projectDir,
iosDeviceDevelopmentBuildProfileName,
false
);
} else {
Log.log('Skipping iOS device development build job...');
}
}
if (iosDeviceDevelopmentBuildProfileName) {
Log.log(`Using iOS device development build profile: ${iosDeviceDevelopmentBuildProfileName}`);
workflowStarter.template.jobs.ios_development_build = {
name: `Build ${iosDeviceDevelopmentBuildProfileName} for iOS simulator`,
type: 'build',
params: {
profile: iosDeviceDevelopmentBuildProfileName,
platform: 'ios',
},
};
nextSteps.add(nextStepForDeviceBuildProfile(iosDeviceDevelopmentBuildProfileName));
}
workflowStarter.nextSteps = [...nextSteps];
return workflowStarter;
}
export async function ensureProductionBuildProfileExistsAsync(
projectDir: string,
workflowStarter: WorkflowStarter
): Promise<WorkflowStarter> {
await addProductionBuildProfileToEasJsonIfNeededAsync(projectDir);
workflowStarter.nextSteps = [
nextStepForDeviceBuildProfile('production'),
nextStepForAppSubmission(),
];
return workflowStarter;
}
export async function customizeTemplateIfNeededAsync(
workflowStarter: WorkflowStarter,
projectDir: string,
expoConfig: ExpoConfig
): Promise<any> {
// Ensure EAS Build is configured
switch (workflowStarter.name) {
case WorkflowStarterName.BUILD:
case WorkflowStarterName.DEPLOY:
case WorkflowStarterName.UPDATE:
if (!(await hasBuildConfigureBeenRunAsync({ projectDir, expoConfig }))) {
throw new Error(
'EAS Build is not configured for this project. Please run "eas build:configure" to configure it.'
);
}
break;
default:
break;
}
// Ensure EAS Update is configured
switch (workflowStarter.name) {
case WorkflowStarterName.DEPLOY:
case WorkflowStarterName.UPDATE:
if (!(await hasUpdateConfigureBeenRunAsync({ projectDir, expoConfig }))) {
throw new Error(
'EAS Update is not configured for this project. Please run "eas update:configure" to configure it.'
);
}
break;
default:
break;
}
// Customize template
switch (workflowStarter.name) {
case WorkflowStarterName.BUILD:
Log.debug('Adding build jobs to template...');
return await addBuildJobsToDevelopmentBuildTemplateAsync(projectDir, workflowStarter);
case WorkflowStarterName.DEPLOY:
return await ensureProductionBuildProfileExistsAsync(projectDir, workflowStarter);
default:
return workflowStarter;
}
}
@@ -0,0 +1,208 @@
import { InvalidEasJsonError, MissingEasJsonError } from '@expo/eas-json/build/errors';
import { CombinedError } from '@urql/core';
import { promises as fs } from 'fs';
import path from 'path';
import * as YAML from 'yaml';
import { buildProfileNamesFromProjectAsync } from './buildProfileUtils';
import { getExpoApiWorkflowSchemaURL } from '../../api';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
import Log from '../../log';
import { createValidator, getReadableErrors } from '../../metadata/utils/ajv';
import { WorkflowFile } from '../../utils/workflowFile';
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
const jobTypesWithBuildProfile = new Set(['build', 'repack']);
const buildProfileIsInterpolated = (profileName: string): boolean => {
return profileName.includes('${{') && profileName.includes('}}');
};
export async function validateWorkflowFileAsync(
workflowFileContents: { yamlConfig: string; filePath: string },
projectDir: string,
graphqlClient: ExpoGraphqlClient,
projectId: string
): Promise<void> {
const parsedYaml = parsedYamlFromWorkflowContents(workflowFileContents);
Log.debug(`Parsed workflow: ${JSON.stringify(parsedYaml, null, 2)}`);
// Check if the parsed result is empty or null
Log.debug(`Validating workflow is not empty...`);
validateWorkflowIsNotEmpty(parsedYaml);
const workflowSchema = await fetchWorkflowSchemaAsync();
// Check that all job types are valid
Log.debug(`Validating workflow job types...`);
validateWorkflowJobTypes(parsedYaml, workflowSchema);
// Check for build jobs that do not match any EAS build profiles
Log.debug(`Validating workflow build jobs...`);
await validateWorkflowBuildJobsAsync(parsedYaml, projectDir);
// Check that result passes validation against workflow schema
Log.debug(`Validating workflow structure...`);
validateWorkflowStructure(parsedYaml, workflowSchema);
// Check for other errors using the server-side validation
Log.debug(`Validating workflow on server...`);
await validateWorkflowOnServerAsync(graphqlClient, projectId, workflowFileContents);
}
export function logWorkflowValidationErrors(error: unknown): void {
if (error instanceof MissingEasJsonError) {
throw new Error(
'Workflows require a valid eas.json. Please run "eas build:configure" to create it.'
);
} else if (error instanceof InvalidEasJsonError) {
throw new Error(
'Workflows require a valid eas.json. Please fix the errors in your eas.json and try again.\n\n' +
error.message
);
} else if (error instanceof YAML.YAMLParseError) {
Log.error(`YAML syntax error: ${error.message}`);
} else if (error instanceof CombinedError) {
WorkflowFile.maybePrintWorkflowFileValidationErrors({
error,
});
throw error;
} else if (error instanceof Error) {
Log.error(`Error: ${error.message}`);
} else {
Log.error(`Unexpected error: ${String(error)}`);
}
}
function validateWorkflowIsNotEmpty(parsedYaml: any): void {
if (
parsedYaml === null ||
parsedYaml === undefined ||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
) {
throw new Error('YAML file is empty or contains only comments.');
}
}
async function validateWorkflowOnServerAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
workflowFileContents: { yamlConfig: string }
): Promise<void> {
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
appId: projectId,
yamlConfig: workflowFileContents.yamlConfig,
});
}
async function validateWorkflowBuildJobsAsync(parsedYaml: any, projectDir: string): Promise<void> {
const jobs = jobsFromWorkflow(parsedYaml);
const buildJobs = jobs.filter(job => jobTypesWithBuildProfile.has(job.value.type));
if (buildJobs.length === 0) {
return;
}
const buildProfileNames = await buildProfileNamesFromProjectAsync(projectDir);
const invalidBuildJobs = buildJobs.filter(
job =>
!buildProfileNames.has(job.value.params.profile) &&
// If a profile name is interpolated, we can't check if it's valid until the workflow actually runs
!buildProfileIsInterpolated(job.value.params.profile)
);
if (invalidBuildJobs.length > 0) {
const invalidBuildProfiles = new Set(invalidBuildJobs.map(job => job.value.params.profile));
throw new Error(
`The build jobs in this workflow refer to the following build profiles that are not present in your eas.json file: ${[
...invalidBuildProfiles,
].join(', ')}`
);
}
}
function validateWorkflowJobTypes(parsedYaml: any, workflowJsonSchema: any): void {
const jobs = jobsFromWorkflow(parsedYaml);
const jobTypes = jobTypesFromWorkflowSchema(workflowJsonSchema);
const invalidJobs = jobs.filter(job => job.value.type && !jobTypes.includes(job.value.type));
if (invalidJobs.length > 0) {
throw new Error(
`The following jobs have invalid types: ${invalidJobs
.map(job => job.key)
.join(', ')}. Valid types are: ${jobTypes.join(', ')}`
);
}
}
function validateWorkflowStructure(parsedYaml: any, workflowJsonSchema: any): void {
delete workflowJsonSchema['$schema'];
const ajv = createValidator();
const validate = ajv.compile(workflowJsonSchema);
const result = validate(parsedYaml);
if (!result) {
Log.debug(
JSON.stringify(
{
errors: validate.errors,
},
null,
2
)
);
const readableErrors = getReadableErrors(validate.errors ?? []);
const processedErrors = new Set<string>();
for (const err of readableErrors) {
if (err.message) {
processedErrors.add(err.message);
}
}
throw new Error([...processedErrors].join('\n'));
}
}
export function parsedYamlFromWorkflowContents(workflowFileContents: { yamlConfig: string }): any {
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
return parsedYaml;
}
export function workflowContentsFromParsedYaml(parsedYaml: any): string {
return YAML.stringify(parsedYaml);
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
async function fetchWorkflowSchemaAsync(): Promise<any> {
// EXPO_TESTING_WORKFLOW_SCHEMA_PATH is used only for testing against a different schema
if (process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH) {
const schemaPath = path.resolve(process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH);
Log.debug(`Loading workflow schema from ${schemaPath}`);
const jsonString = await fs.readFile(schemaPath, 'utf-8');
const jsonFromFile = JSON.parse(jsonString);
return jsonFromFile.data;
}
// Otherwise, we fetch from <ApiBaseUrl>/v2/workflows/schema
const schemaUrl = getExpoApiWorkflowSchemaURL();
Log.debug(`Fetching workflow schema from ${schemaUrl}`);
const response = await fetch(schemaUrl);
if (!response.ok) {
throw new Error(`Unable to fetch EAS Workflow schema, received status: ${response.status}`);
}
const jsonResponse: any = (await response.json()) as any;
return jsonResponse.data;
}
function jobsFromWorkflow(parsedYaml: any): any[] {
return Object.entries(parsedYaml?.jobs).flatMap(([key, value]: [string, any]) => {
return {
key,
value,
};
});
}
function jobTypesFromWorkflowSchema(workflowJsonSchema: any): string[] {
return workflowJsonSchema?.properties?.jobs?.additionalProperties?.anyOf.map(
(props: any) => props.properties.type.const
);
}
@@ -10,10 +10,10 @@ import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform';
import { isExpoUpdatesInstalled, isUsingEASUpdate } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { ensureEASUpdateIsConfiguredInEasJsonAsync } from '../../update/configure';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { promptPlatformAsync } from '../../utils/prompts';
export default class BuildConfigure extends EasCommand {
static override description = 'configure the project to support EAS Build';
@@ -113,24 +113,7 @@ export default class BuildConfigure extends EasCommand {
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
Log.addNewLineIfNone();
const { platform } = await promptAsync({
type: 'select',
return await promptPlatformAsync({
message: 'Which platforms would you like to configure for EAS Build?',
name: 'platform',
choices: [
{
title: 'All',
value: RequestedPlatform.All,
},
{
title: 'iOS',
value: RequestedPlatform.Ios,
},
{
title: 'Android',
value: RequestedPlatform.Android,
},
],
});
return platform;
}
@@ -22,7 +22,6 @@ import {
BuildStatus,
BuildWorkflow,
DistributionType,
EnvironmentVariableEnvironment,
IosJobOverridesInput,
ProjectArchiveSourceType,
StatuspageServiceName,
@@ -48,7 +47,7 @@ interface BuildResignFlags {
sourceProfile?: string;
maybeBuildId?: string;
wait: boolean;
environment?: EnvironmentVariableEnvironment;
environment?: string;
}
interface RawBuildResignFlags {
json: boolean;
@@ -60,7 +59,7 @@ interface RawBuildResignFlags {
'source-profile': string | undefined;
wait: boolean;
id: string | undefined;
environment: EnvironmentVariableEnvironment | undefined;
environment: string | undefined;
}
export default class BuildResign extends EasCommand {
+10 -7
View File
@@ -8,7 +8,6 @@ import * as path from 'node:path';
import { getHostingDeploymentsUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
@@ -45,7 +44,7 @@ interface DeployFlags {
json: boolean;
isProduction: boolean;
aliasName?: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
deploymentIdentifier?: string;
exportDir: string;
dryRun: boolean;
@@ -53,7 +52,7 @@ interface DeployFlags {
interface RawDeployFlags {
'non-interactive': boolean;
environment?: EnvironmentVariableEnvironment;
environment?: string;
json: boolean;
prod: boolean;
alias?: string;
@@ -138,6 +137,7 @@ export default class WorkerDeploy extends EasCommand {
logExportedProjectInfo(projectDist);
async function* emitWorkerTarballAsync(params: {
routesConfig: WorkerAssets.RoutesConfigEntry | null;
assetMap: WorkerAssets.AssetMap;
manifest: WorkerAssets.Manifest;
}): AsyncGenerator<WorkerAssets.FileEntry> {
@@ -148,6 +148,8 @@ export default class WorkerDeploy extends EasCommand {
for await (const workerFile of workerFiles) {
yield [`server/${workerFile.normalizedPath}`, workerFile.data];
}
} else if (projectDist.type === 'static' && params.routesConfig) {
yield ['routes.json', JSON.stringify(params.routesConfig)];
}
}
@@ -284,12 +286,13 @@ export default class WorkerDeploy extends EasCommand {
manifestResult.conflictingVariableNames.join(' ')
);
}
assetFiles = await WorkerAssets.collectAssetsAsync(
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path,
{ maxFileSize: MAX_UPLOAD_SIZE }
);
const assetPath = projectDist.type === 'server' ? projectDist.clientPath : projectDist.path;
assetFiles = await WorkerAssets.collectAssetsAsync(assetPath, {
maxFileSize: MAX_UPLOAD_SIZE,
});
tarPath = await WorkerAssets.packFilesIterableAsync(
emitWorkerTarballAsync({
routesConfig: await WorkerAssets.getRoutesConfigAsync(assetPath),
assetMap: WorkerAssets.assetsToAssetsMap(assetFiles),
manifest: manifestResult.manifest,
})
+51 -16
View File
@@ -1,11 +1,11 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
@@ -47,6 +47,7 @@ describe(EnvCreate, () => {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
type: EnvironmentSecretType.String,
}));
jest
.mocked(EnvironmentVariableMutation.createSharedVariableAsync)
@@ -56,6 +57,7 @@ describe(EnvCreate, () => {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
type: EnvironmentSecretType.String,
}));
jest
.mocked(EnvironmentVariableMutation.updateAsync)
@@ -101,7 +103,7 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Secret,
type: EnvironmentSecretType.String,
},
@@ -138,7 +140,7 @@ describe(EnvCreate, () => {
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Project,
},
]);
@@ -166,7 +168,7 @@ describe(EnvCreate, () => {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Secret,
});
});
@@ -199,8 +201,7 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
isGlobal: true,
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
@@ -235,7 +236,7 @@ describe(EnvCreate, () => {
.mockImplementation(async () => [
{
id: 'otherId',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
@@ -273,7 +274,7 @@ describe(EnvCreate, () => {
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
@@ -284,7 +285,7 @@ describe(EnvCreate, () => {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
});
@@ -319,12 +320,8 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
environments: [
EnvironmentVariableEnvironment.Production,
EnvironmentVariableEnvironment.Development,
],
environments: [DefaultEnvironment.Production, DefaultEnvironment.Development],
visibility: EnvironmentVariableVisibility.Public,
isGlobal: true,
type: EnvironmentSecretType.String,
},
testAccountId
@@ -343,7 +340,7 @@ describe(EnvCreate, () => {
jest
.mocked(promptVariableEnvironmentAsync)
// @ts-expect-error
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
.mockImplementation(async () => [DefaultEnvironment.Production]);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -361,7 +358,7 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
@@ -369,4 +366,42 @@ describe(EnvCreate, () => {
);
});
});
it('accepts development environment when using positional argument', async () => {
const command = new EnvCreate(
[
'development',
'--name',
'TEST_VAR',
'--value',
'test-value',
'--visibility',
'plaintext',
'--scope',
'project',
'--non-interactive',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'TEST_VAR',
value: 'test-value',
environments: [DefaultEnvironment.Development],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testProjectId
);
});
});
@@ -1,8 +1,10 @@
import { Config } from '@oclif/core';
import { DefaultEnvironment } from '../../../build/utils/environment';
import {
EnvironmentVariableEnvironment,
EnvironmentSecretType,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
@@ -35,7 +37,7 @@ describe(EnvDelete, () => {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
@@ -121,4 +123,40 @@ describe(EnvDelete, () => {
expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled();
});
it('accepts development environment when using positional argument', async () => {
const mockVariable = {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([mockVariable]);
jest.mocked(EnvironmentVariableMutation.deleteAsync).mockResolvedValueOnce({ id: 'var1' });
jest.spyOn(Log, 'log').mockImplementation(() => {});
const command = new EnvDelete(
['development', '--variable-name', 'TEST_VAR_1', '--scope', 'project', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
environment: DefaultEnvironment.Development,
});
});
});
@@ -0,0 +1,85 @@
import { Config } from '@oclif/core';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import EnvExec from '../exec';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../log');
describe(EnvExec, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const mockEnvironmentVariables = [
{
id: 'var1',
name: 'EXPO_PUBLIC_API_URL',
value: 'https://api.example.com',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
];
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
// Mock GraphQL query
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValue(mockEnvironmentVariables);
});
it('accepts development environment when using positional argument', async () => {
const command = new EnvExec(['development', 'echo $EXPO_PUBLIC_API_URL'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: DefaultEnvironment.Development,
}
);
});
it('accepts custom environment when using positional argument', async () => {
const command = new EnvExec(['custom-environment', 'echo $EXPO_PUBLIC_API_URL'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: 'custom-environment',
}
);
});
});
+34 -4
View File
@@ -1,10 +1,10 @@
import { Config } from '@oclif/core';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
@@ -27,7 +27,7 @@ describe(EnvGet, () => {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -82,7 +82,7 @@ describe(EnvGet, () => {
it('prompts for variable name and environment if the name is ambigous', async () => {
jest
.mocked(promptVariableEnvironmentAsync)
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
.mockResolvedValueOnce(DefaultEnvironment.Production);
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
const command = new EnvGet([], mockConfig);
@@ -98,7 +98,7 @@ describe(EnvGet, () => {
id: 'var2',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -111,4 +111,34 @@ describe(EnvGet, () => {
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
expect(promptVariableNameAsync).toHaveBeenCalled();
});
it('accepts development environment when using positional argument', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
jest.spyOn(Log, 'log').mockImplementation(() => {});
const command = new EnvGet(
['development', '--variable-name', 'TEST_VAR_1', '--scope', 'project'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: DefaultEnvironment.Development,
filterNames: ['TEST_VAR_1'],
includeFileContent: true,
}
);
});
});
+42 -28
View File
@@ -1,11 +1,11 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
@@ -13,10 +13,12 @@ import {
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptVariableEnvironmentAsync } from '../../../utils/prompts';
import EnvList from '../list';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../utils/prompts');
jest.mock('../../../log');
const mockVariables: EnvironmentVariableFragment[] = [
@@ -24,7 +26,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
@@ -35,7 +37,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
id: 'var2',
name: 'TEST_VAR_2',
value: 'value2',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
@@ -51,27 +53,19 @@ describe(EnvList, () => {
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('lists project environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvList([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
async (args: any) => {
if (args.nonInteractive) {
throw new Error(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
}
if (args.multiple) {
return [DefaultEnvironment.Production];
}
return DefaultEnvironment.Production;
}
);
});
it('lists project environment variables in specified environments', async () => {
@@ -88,13 +82,33 @@ describe(EnvList, () => {
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Production,
environment: DefaultEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('accepts development environment when using positional argument', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvList(['development'], mockConfig); // Using positional argument
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: DefaultEnvironment.Development,
includeFileContent: false,
});
});
it('lists project environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
@@ -113,7 +127,7 @@ describe(EnvList, () => {
graphqlClient,
{
appId: testProjectId,
environment: undefined,
environment: DefaultEnvironment.Production,
includeFileContent: false,
}
);
@@ -135,7 +149,7 @@ describe(EnvList, () => {
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
environment: DefaultEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
@@ -158,7 +172,7 @@ describe(EnvList, () => {
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
environment: DefaultEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
@@ -0,0 +1,388 @@
import { Config } from '@oclif/core';
import * as fs from 'fs-extra';
import path from 'path';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import {
EnvironmentVariableWithFileContent,
EnvironmentVariablesQuery,
} from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import EnvPull from '../pull';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../prompts');
jest.mock('fs-extra');
jest.mock('../../../log');
describe(EnvPull, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const testProjectDir = '/test/project';
const testTargetPath = '.env.local';
const mockEnvironmentVariables: EnvironmentVariableWithFileContent[] = [
{
id: 'var1',
name: 'EXPO_PUBLIC_API_URL',
value: 'https://api.example.com',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
{
id: 'var2',
name: 'DATABASE_URL',
value: 'postgres://localhost:5432/mydb',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
},
{
id: 'var3',
name: 'SECRET_KEY',
value: 'super-secret-key',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Secret,
type: EnvironmentSecretType.String,
},
{
id: 'var4',
name: 'CONFIG_FILE',
value: 'base64-encoded-file-content',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.FileBase64,
valueWithFileContent: Buffer.from('{"key": "value"}').toString('base64'),
},
];
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'addNewLineIfNone').mockImplementation(() => {});
// Mock fs-extra methods
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(false));
jest.mocked(fs.writeFile).mockImplementation(() => Promise.resolve());
jest.mocked(fs.mkdir).mockImplementation(() => Promise.resolve());
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
// Mock GraphQL query
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValue(mockEnvironmentVariables);
});
describe('environment validation', () => {
it('accepts development environment', async () => {
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: 'development',
includeFileContent: true,
}
);
});
it('accepts custom environment', async () => {
const command = new EnvPull(['custom-environment'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: 'custom-environment',
includeFileContent: true,
}
);
});
});
describe('file operations', () => {
it('writes environment variables to .env.local file', async () => {
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(fs.writeFile).toHaveBeenCalledWith(
testTargetPath,
expect.stringContaining('# Environment: development')
);
});
it('writes correct environment variable content', async () => {
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Find the call that writes to .env.local (not the file variable)
const writeFileCalls = jest.mocked(fs.writeFile).mock.calls;
const envFileCall = writeFileCalls.find(call => call[0] === testTargetPath);
expect(envFileCall).toBeDefined();
const fileContent = envFileCall![1] as string;
// Check that the file contains the expected content
expect(fileContent).toContain('# Environment: development');
expect(fileContent).toContain('EXPO_PUBLIC_API_URL=https://api.example.com');
expect(fileContent).toContain('DATABASE_URL=postgres://localhost:5432/mydb');
expect(fileContent).toContain('# SECRET_KEY=***** (secret)');
});
it('handles file variables correctly', async () => {
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should create .eas/.env directory for file variables
expect(fs.mkdir).toHaveBeenCalledWith(path.join(testProjectDir, '.eas', '.env'), {
recursive: true,
});
// Should write the file content (base64 encoded)
expect(fs.writeFile).toHaveBeenCalledWith(
path.join(testProjectDir, '.eas', '.env', 'CONFIG_FILE'),
Buffer.from('{"key": "value"}').toString('base64'),
'base64'
);
// Should include file path in env file
const writeFileCalls = jest.mocked(fs.writeFile).mock.calls;
const envFileCall = writeFileCalls.find(call => call[0] === testTargetPath);
expect(envFileCall).toBeDefined();
const fileContent = envFileCall![1] as string;
expect(fileContent).toContain('CONFIG_FILE=');
expect(fileContent).toContain(path.join(testProjectDir, '.eas', '.env', 'CONFIG_FILE'));
});
it('prompts for overwrite when file exists in interactive mode', async () => {
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
jest.mocked(confirmAsync).mockResolvedValue(true);
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(confirmAsync).toHaveBeenCalledWith({
message: `File ${testTargetPath} already exists. Do you want to overwrite it?`,
});
});
it('aborts when user declines overwrite', async () => {
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
jest.mocked(confirmAsync).mockResolvedValue(false);
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await expect(command.runAsync()).rejects.toThrow(`File ${testTargetPath} already exists.`);
});
});
describe('non-interactive mode', () => {
it('works in non-interactive mode', async () => {
const command = new EnvPull(['development', '--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: 'development',
includeFileContent: true,
}
);
});
it('requires environment in non-interactive mode', async () => {
const command = new EnvPull(['--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await expect(command.runAsync()).rejects.toThrow(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
});
});
describe('custom target path', () => {
it('writes to custom path when specified', async () => {
const customPath = '.env.custom';
const command = new EnvPull(['development', '--path', customPath], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(fs.writeFile).toHaveBeenCalledWith(
customPath,
expect.stringContaining('# Environment: development')
);
});
});
describe('logging', () => {
it('logs success message with correct environment', async () => {
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(Log.log).toHaveBeenCalledWith(
`Pulled plain text and sensitive environment variables from "development" environment to ${testTargetPath}.`
);
});
it('logs secret variables that were skipped', async () => {
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(Log.log).toHaveBeenCalledWith(
expect.stringContaining('The following variables have the secret visibility')
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('SECRET_KEY'));
});
});
describe('existing .env.local file handling', () => {
it('preserves existing secret values when they exist', async () => {
const existingEnvContent = 'SECRET_KEY=existing-secret-value\nOTHER_VAR=other-value';
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(existingEnvContent));
jest.mocked(confirmAsync).mockResolvedValue(true);
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Find the call that writes to .env.local (not the file variable)
const writeFileCalls = jest.mocked(fs.writeFile).mock.calls;
const envFileCall = writeFileCalls.find(call => call[0] === testTargetPath);
expect(envFileCall).toBeDefined();
const fileContent = envFileCall![1] as string;
// Should use existing secret value instead of placeholder
expect(fileContent).toContain('SECRET_KEY=existing-secret-value');
expect(fileContent).not.toContain('# SECRET_KEY=***** (secret)');
// Should log that it reused the local value
expect(Log.log).toHaveBeenCalledWith(
expect.stringContaining('Reused local values for following secrets: SECRET_KEY')
);
});
});
});
@@ -0,0 +1,279 @@
import { Config } from '@oclif/core';
import * as fs from 'fs-extra';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import EnvPush from '../push';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('fs-extra');
jest.mock('../../../log');
jest.mock('../../../prompts');
describe(EnvPush, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const testProjectDir = '/test/project';
const testEnvPath = '.env.test';
const mockEnvContent = `EXPO_PUBLIC_API_URL=https://api.example.com
VARIABLE_NAME=variable value
SECRET_KEY=super-secret-key`;
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.spyOn(Log, 'error').mockImplementation(() => {});
// Mock fs-extra methods
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(mockEnvContent));
// Mock GraphQL queries and mutations
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([]); // No existing variables
jest
.mocked(EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync)
.mockResolvedValue(true);
jest.mocked(confirmAsync).mockResolvedValue(true);
});
it('accepts development environment when using positional argument', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([]);
const command = new EnvPush(['development', '--path', testEnvPath], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: DefaultEnvironment.Development,
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
});
});
it('accepts custom environment', async () => {
const command = new EnvPush(['custom-environment', '--path', testEnvPath], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: 'custom-environment',
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
});
});
describe('--force option', () => {
it('automatically overrides existing variables without confirmation when --force is used', async () => {
const existingVariable = {
id: 'var1',
name: 'VARIABLE_NAME',
value: 'old variable value',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should not call confirmAsync for existing variables
expect(confirmAsync).not.toHaveBeenCalled();
// Should log that force flag is being used
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding existing variables.'
);
// Should call the mutation with overwrite flag
expect(
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
).toHaveBeenCalledWith(
graphqlClient,
expect.arrayContaining([
expect.objectContaining({
name: 'VARIABLE_NAME',
value: 'variable value',
environments: [DefaultEnvironment.Development],
overwrite: true,
}),
]),
testProjectId
);
});
it('automatically overrides sensitive variables without confirmation when --force is used', async () => {
const existingSensitiveVariable = {
id: 'var1',
name: 'SECRET_KEY',
value: 'old-secret-key',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
.mockResolvedValue([existingSensitiveVariable]);
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should not call confirmAsync for sensitive variables
expect(confirmAsync).not.toHaveBeenCalled();
// Should log that force flag is being used for sensitive variables
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding sensitive variables.'
);
// Should call the mutation with overwrite flag
expect(
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
).toHaveBeenCalledWith(
graphqlClient,
expect.arrayContaining([
expect.objectContaining({
name: 'SECRET_KEY',
value: 'super-secret-key',
environments: [DefaultEnvironment.Development],
overwrite: true,
}),
]),
testProjectId
);
});
it('logs appropriate messages when using --force flag', async () => {
const existingVariable = {
id: 'var1',
name: 'VARIABLE_NAME',
value: 'old variable value',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should log both force messages
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding existing variables.'
);
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding sensitive variables.'
);
});
it('still prompts for confirmation when --force is not used', async () => {
const existingVariable = {
id: 'var1',
name: 'VARIABLE_NAME',
value: 'old variable value',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
jest.mocked(confirmAsync).mockResolvedValue(true);
const command = new EnvPush(['development', '--path', testEnvPath], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should call confirmAsync for both existing variables and sensitive variables
expect(confirmAsync).toHaveBeenCalledTimes(2);
expect(confirmAsync).toHaveBeenCalledWith({
message:
'The VARIABLE_NAME environment variable already exists in development environment. Do you want to override it?',
});
expect(confirmAsync).toHaveBeenCalledWith({
message:
'You are about to overwrite sensitive variables.\n- VARIABLE_NAME\n Do you want to continue?',
});
// Should not log force messages
expect(Log.log).not.toHaveBeenCalledWith(
'Using --force flag: automatically overriding existing variables.'
);
expect(Log.log).not.toHaveBeenCalledWith(
'Using --force flag: automatically overriding sensitive variables.'
);
});
});
});
@@ -2,8 +2,9 @@ import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { DefaultEnvironment } from '../../../build/utils/environment';
import {
EnvironmentVariableEnvironment,
EnvironmentSecretType,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
@@ -11,12 +12,18 @@ import { EnvironmentVariableMutation } from '../../../graphql/mutations/Environm
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import {
parseVisibility,
promptVariableEnvironmentAsync,
promptVariableVisibilityAsync,
} from '../../../utils/prompts';
import EnvUpdate from '../update';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../utils/prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
@@ -33,6 +40,41 @@ describe(EnvUpdate, () => {
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
async (args: any) => {
if (args.nonInteractive) {
throw new Error(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
}
if (args.multiple) {
return ['production'];
}
return 'production';
}
);
(jest.mocked(promptVariableVisibilityAsync) as jest.MockedFunction<any>).mockResolvedValue(
EnvironmentVariableVisibility.Public
);
(jest.mocked(parseVisibility) as jest.MockedFunction<any>).mockImplementation(
(visibility: string) => {
switch (visibility) {
case 'plaintext':
return EnvironmentVariableVisibility.Public;
case 'sensitive':
return EnvironmentVariableVisibility.Sensitive;
case 'secret':
return EnvironmentVariableVisibility.Secret;
default:
throw new Error(`Invalid visibility: ${visibility}`);
}
}
);
(jest.mocked(selectAsync) as jest.MockedFunction<any>).mockResolvedValue({
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
});
});
it('updates a project variable by selected name in non-interactive mode', async () => {
@@ -41,7 +83,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
@@ -73,7 +115,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
expect(Log.withTick).toHaveBeenCalledWith(
@@ -87,7 +129,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
@@ -119,7 +161,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
});
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on account testuser.`
@@ -129,6 +171,7 @@ describe(EnvUpdate, () => {
it('prompts for variable selection when selected name is not provided', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
{ id: '2', name: 'ANOTHER_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
@@ -140,7 +183,7 @@ describe(EnvUpdate, () => {
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ id: variableId })
@@ -162,4 +205,50 @@ describe(EnvUpdate, () => {
'Variable with name NON_EXISTENT_VARIABLE does not exist on project @testuser/testpp.'
);
});
it('accepts development environment when using positional argument', async () => {
const mockVariable = {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([mockVariable]);
jest.mocked(EnvironmentVariableMutation.updateAsync).mockResolvedValueOnce(mockVariable);
jest.spyOn(Log, 'log').mockImplementation(() => {});
const command = new EnvUpdate(
[
'development',
'--variable-name',
'TEST_VAR_1',
'--value',
'new-value',
'--scope',
'project',
'--non-interactive',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
environment: DefaultEnvironment.Development,
filterNames: ['TEST_VAR_1'],
});
});
});
+22 -25
View File
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
@@ -13,7 +14,6 @@ import {
} from '../../commandUtils/flags';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../graphql/generated';
@@ -33,7 +33,6 @@ import {
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
interface RawCreateFlags {
name?: string;
@@ -42,7 +41,7 @@ interface RawCreateFlags {
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
scope: EASEnvironmentVariableScopeFlagValue;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
'non-interactive': boolean;
}
@@ -53,7 +52,7 @@ interface CreateFlags {
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
scope: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
'non-interactive': boolean;
}
@@ -64,7 +63,7 @@ export default class EnvCreate extends EasCommand {
{
name: 'environment',
description:
"Environment to create the variable in. One of 'production', 'preview', or 'development'.",
"Environment to create the variable in. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -101,6 +100,13 @@ export default class EnvCreate extends EasCommand {
const validatedFlags = this.sanitizeFlags(flags);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvCreate, {
nonInteractive: validatedFlags['non-interactive'],
});
const {
name,
value,
@@ -111,14 +117,7 @@ export default class EnvCreate extends EasCommand {
force,
type,
fileName,
} = await this.promptForMissingFlagsAsync(validatedFlags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvCreate, {
nonInteractive,
});
} = await this.promptForMissingFlagsAsync(validatedFlags, args, { graphqlClient, projectId });
const [projectDisplayName, ownerAccount] = await Promise.all([
getDisplayNameForProjectIdAsync(graphqlClient, projectId),
@@ -220,7 +219,6 @@ export default class EnvCreate extends EasCommand {
value,
visibility,
environments,
isGlobal: true, // TODO: every account-wide variable is global for now so it's not user facing
type: type ?? EnvironmentSecretType.String,
},
ownerAccount.id
@@ -273,7 +271,8 @@ export default class EnvCreate extends EasCommand {
type,
...rest
}: CreateFlags,
{ environment }: { environment?: string }
{ environment }: { environment?: string },
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
): Promise<
Required<
Omit<CreateFlags, 'type' | 'visibility'> & {
@@ -324,18 +323,16 @@ export default class EnvCreate extends EasCommand {
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
if (environment && !isEnvironment(environment.toUpperCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
let newEnvironments = environments
? environments
: environment
? [environment.toUpperCase() as EnvironmentVariableEnvironment]
: undefined;
let newEnvironments = environments ? environments : environment ? [environment] : undefined;
if (!newEnvironments) {
newEnvironments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
newEnvironments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
canEnterCustomEnvironment: true,
graphqlClient,
projectId,
});
if (!newEnvironments || newEnvironments.length === 0) {
throw new Error('No environments selected');
+6 -13
View File
@@ -9,23 +9,23 @@ import {
EASNonInteractiveFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
import { EnvironmentVariableScope } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
import { formatVariableName } from '../../utils/variableUtils';
interface DeleteFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
scope: EnvironmentVariableScope;
}
interface RawDeleteFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
scope: EASEnvironmentVariableScopeFlagValue;
}
@@ -37,7 +37,7 @@ export default class EnvDelete extends EasCommand {
'variable-name': Flags.string({
description: 'Name of the variable to delete',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to delete',
}),
@@ -49,7 +49,7 @@ export default class EnvDelete extends EasCommand {
{
name: 'environment',
description:
"Current environment of the variable to delete. One of 'production', 'preview', or 'development'.",
"Current environment of the variable to delete. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -165,13 +165,6 @@ export default class EnvDelete extends EasCommand {
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toUpperCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return { ...flags, 'variable-environment': environment, scope };
}
+9 -13
View File
@@ -4,21 +4,19 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
type ParsedFlags =
| {
nonInteractive: true;
environment: EnvironmentVariableEnvironment;
environment: string;
command: string;
}
| {
nonInteractive: false;
environment?: EnvironmentVariableEnvironment;
environment?: string;
command: string;
};
@@ -44,7 +42,7 @@ export default class EnvExec extends EasCommand {
name: 'environment',
required: true,
description:
"Environment to execute the command in. One of 'production', 'preview', or 'development'.",
"Environment to execute the command in. Default environments are 'production', 'preview', and 'development'.",
},
{
name: 'bash_command',
@@ -70,7 +68,11 @@ export default class EnvExec extends EasCommand {
const environment =
parsedFlags.environment ??
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
(await promptVariableEnvironmentAsync({
nonInteractive: parsedFlags.nonInteractive,
graphqlClient,
projectId,
}));
const environmentVariables = await this.loadEnvironmentVariablesAsync({
graphqlClient,
projectId,
@@ -101,12 +103,6 @@ export default class EnvExec extends EasCommand {
);
}
environment = environment?.toUpperCase();
if (!isEnvironment(environment)) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const firstChar = bash_command[0];
const lastChar = bash_command[bash_command.length - 1];
const cleanCommand =
@@ -198,7 +194,7 @@ export default class EnvExec extends EasCommand {
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
environment: EnvironmentVariableEnvironment;
environment: string;
nonInteractive: boolean;
}): Promise<Record<string, string>> {
const environmentVariablesQueryResult =
+8 -13
View File
@@ -11,7 +11,6 @@ import {
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
@@ -19,11 +18,11 @@ import {
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';
import { formatVariable, formatVariableValue } from '../../utils/variableUtils';
interface RawGetFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
format?: string;
scope: EASEnvironmentVariableScopeFlagValue;
@@ -31,7 +30,7 @@ interface RawGetFlags {
interface GetFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
format?: string;
scope: EnvironmentVariableScope;
@@ -49,7 +48,7 @@ export default class EnvGet extends EasCommand {
{
name: 'environment',
description:
"Current environment of the variable. One of 'production', 'preview', or 'development'.",
"Current environment of the variable. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -58,7 +57,7 @@ export default class EnvGet extends EasCommand {
'variable-name': Flags.string({
description: 'Name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable',
}),
@@ -93,6 +92,8 @@ export default class EnvGet extends EasCommand {
environment = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: false,
graphqlClient,
projectId,
});
}
@@ -156,12 +157,6 @@ export default class EnvGet extends EasCommand {
? EnvironmentVariableScope.Shared
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toUpperCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return {
...flags,
'variable-environment': environment,
@@ -177,7 +172,7 @@ async function getVariablesAsync(
scope: string,
projectId: string,
name: string | undefined,
environment: EnvironmentVariableEnvironment | undefined
environment: string | undefined
): Promise<EnvironmentVariableFragment[]> {
if (!name) {
throw new Error(
+12 -12
View File
@@ -9,7 +9,7 @@ import {
EASMultiEnvironmentFlag,
EASVariableFormatFlag,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
import { EnvironmentVariableScope } from '../../graphql/generated';
import {
EnvironmentVariableWithFileContent,
EnvironmentVariablesQuery,
@@ -19,7 +19,6 @@ import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import {
formatVariable,
formatVariableValue,
isEnvironment,
performForEnvironmentsAsync,
} from '../../utils/variableUtils';
@@ -35,7 +34,7 @@ async function getVariablesForScopeAsync(
scope: EnvironmentVariableScope;
includingSensitive: boolean;
includeFileContent: boolean;
environment?: EnvironmentVariableEnvironment;
environment?: string;
projectId: string;
}
): Promise<EnvironmentVariableWithFileContent[]> {
@@ -70,7 +69,7 @@ async function getVariablesForScopeAsync(
interface RawListFlags {
scope: EASEnvironmentVariableScopeFlagValue;
format: string;
environment: EnvironmentVariableEnvironment[] | undefined;
environment: string[] | undefined;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive'?: boolean;
@@ -79,7 +78,7 @@ interface RawListFlags {
interface ListFlags {
scope: EnvironmentVariableScope;
format: string;
environment: EnvironmentVariableEnvironment[] | undefined;
environment: string[] | undefined;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive': boolean;
@@ -111,7 +110,7 @@ export default class EnvList extends EasCommand {
{
name: 'environment',
description:
"Environment to list the variables from. One of 'production', 'preview', or 'development'.",
"Environment to list the variables from. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -136,7 +135,12 @@ export default class EnvList extends EasCommand {
});
if (!environments) {
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
graphqlClient,
projectId,
});
}
await performForEnvironmentsAsync(environments, async environment => {
@@ -179,14 +183,10 @@ export default class EnvList extends EasCommand {
flags: RawListFlags,
{ environment }: { environment?: string }
): ListFlags {
if (environment && !isEnvironment(environment.toUpperCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const environments = flags.environment
? flags.environment
: environment
? [environment.toUpperCase() as EnvironmentVariableEnvironment]
? [environment]
: undefined;
return {
+10 -11
View File
@@ -13,7 +13,6 @@ import {
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
export default class EnvPull extends EasCommand {
static override description =
@@ -29,7 +28,7 @@ export default class EnvPull extends EasCommand {
{
name: 'environment',
description:
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
"Environment to pull variables from. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -49,15 +48,7 @@ export default class EnvPull extends EasCommand {
flags: { environment: flagEnvironment, path: targetPath, 'non-interactive': nonInteractive },
} = await this.parse(EnvPull);
let environment = flagEnvironment?.toUpperCase() ?? argEnvironment?.toUpperCase();
if (!environment) {
environment = await promptVariableEnvironmentAsync({ nonInteractive });
}
if (!isEnvironment(environment)) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
let environment = flagEnvironment?.toLowerCase() ?? argEnvironment?.toLowerCase();
const {
projectId,
@@ -67,6 +58,14 @@ export default class EnvPull extends EasCommand {
nonInteractive,
});
if (!environment) {
environment = await promptVariableEnvironmentAsync({
nonInteractive,
graphqlClient,
projectId,
});
}
targetPath = targetPath ?? '.env.local';
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
+60 -52
View File
@@ -6,20 +6,16 @@ import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { EASMultiEnvironmentFlag } from '../../commandUtils/flags';
import {
EnvironmentVariableEnvironment,
CreateEnvironmentVariableInput,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../graphql/generated';
import {
EnvironmentVariableMutation,
EnvironmentVariablePushInput,
} from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
export default class EnvPush extends EasCommand {
static override description =
@@ -36,13 +32,17 @@ export default class EnvPush extends EasCommand {
description: 'Path to the input `.env` file',
default: '.env.local',
}),
force: Flags.boolean({
description: 'Skip confirmation and automatically override existing variables',
default: false,
}),
};
static override args = [
{
name: 'environment',
description:
"Environment to push variables to. One of 'production', 'preview', or 'development'.",
"Environment to push variables to. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -50,7 +50,7 @@ export default class EnvPush extends EasCommand {
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvPush);
let { environment: environments, path: envPath } = this.parseFlagsAndArgs(flags, args);
let { environment: environments, path: envPath, force } = this.parseFlagsAndArgs(flags, args);
const {
projectId,
@@ -63,11 +63,13 @@ export default class EnvPush extends EasCommand {
environments = await promptVariableEnvironmentAsync({
nonInteractive: false,
multiple: true,
canEnterCustomEnvironment: true,
graphqlClient,
projectId,
});
}
const updateVariables: Record<string, EnvironmentVariablePushInput> =
await this.parseEnvFileAsync(envPath, environments);
const updateVariables = await this.parseEnvFileAsync(envPath, environments);
const variableNames = Object.keys(updateVariables);
@@ -117,40 +119,46 @@ export default class EnvPush extends EasCommand {
Log.warn(`Some variables already exist in the ${displayedEnvironment} environment.`);
const variableNames = existingDifferentVariables.map(variable => variable.name);
const confirmationMessage =
variableNames.length > 1
? `The ${variableNames.join(
', '
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
const confirm = await confirmAsync({
message: confirmationMessage,
});
let variablesToOverwrite: string[] = [];
if (!confirm && existingDifferentVariables.length === 0) {
throw new Error('No new variables to push.');
}
if (confirm) {
if (force) {
// When --force is used, automatically override all existing variables
Log.log('Using --force flag: automatically overriding existing variables.');
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
} else {
const promptResult = await promptAsync({
type: 'multiselect',
name: 'variablesToOverwrite',
message: 'Select variables to overwrite:',
// @ts-expect-error property missing from `@types/prompts`
optionsPerPage: 20,
choices: existingDifferentVariables.map(variable => ({
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
variable.value ?? '(secret)'
})`,
value: variable.name,
})),
const confirmationMessage =
variableNames.length > 1
? `The ${variableNames.join(
', '
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
const confirm = await confirmAsync({
message: confirmationMessage,
});
variablesToOverwrite = promptResult.variablesToOverwrite;
if (!confirm && existingDifferentVariables.length === 0) {
throw new Error('No new variables to push.');
}
if (confirm) {
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
} else {
const promptResult = await promptAsync({
type: 'multiselect',
name: 'variablesToOverwrite',
message: 'Select variables to overwrite:',
// @ts-expect-error property missing from `@types/prompts`
optionsPerPage: 20,
choices: existingDifferentVariables.map(variable => ({
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
variable.value ?? '(secret)'
})`,
value: variable.name,
})),
});
variablesToOverwrite = promptResult.variablesToOverwrite;
}
}
for (const existingVariable of existingVariables) {
@@ -168,7 +176,7 @@ export default class EnvPush extends EasCommand {
variable => variable.visibility !== EnvironmentVariableVisibility.Public
);
if (existingSensitiveVariables.length > 0) {
if (existingSensitiveVariables.length > 0 && !force) {
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
variable => `- ${variable.name}`
);
@@ -180,6 +188,8 @@ export default class EnvPush extends EasCommand {
if (!confirm) {
throw new Error('Aborting...');
}
} else if (existingSensitiveVariables.length > 0 && force) {
Log.log('Using --force flag: automatically overriding sensitive variables.');
}
}
@@ -199,16 +209,14 @@ export default class EnvPush extends EasCommand {
}
parseFlagsAndArgs(
flags: { path: string; environment: EnvironmentVariableEnvironment[] | undefined },
flags: {
path: string;
environment: string[] | undefined;
force: boolean;
},
{ environment }: Record<string, string>
): { environment?: EnvironmentVariableEnvironment[]; path: string } {
if (environment && !isEnvironment(environment.toUpperCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const environments =
flags.environment ??
(environment ? [environment.toUpperCase() as EnvironmentVariableEnvironment] : undefined);
): { environment?: string[]; path: string; force: boolean } {
const environments = flags.environment ?? (environment ? [environment] : undefined);
return {
...flags,
@@ -218,12 +226,12 @@ export default class EnvPush extends EasCommand {
private async parseEnvFileAsync(
envPath: string,
environments: EnvironmentVariableEnvironment[]
): Promise<Record<string, EnvironmentVariablePushInput>> {
environments: string[]
): Promise<Record<string, CreateEnvironmentVariableInput & { environments: string[] }>> {
if (!(await fs.exists(envPath))) {
throw new Error(`File ${envPath} does not exist.`);
}
const pushInput: Record<string, EnvironmentVariablePushInput> = {};
const pushInput: Awaited<ReturnType<typeof this.parseEnvFileAsync>> = {};
const variables: Record<string, string> = dotenv.parse(await fs.readFile(envPath, 'utf8'));
+26 -24
View File
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
@@ -15,7 +16,6 @@ import {
} from '../../commandUtils/flags';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
@@ -36,17 +36,17 @@ import {
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
import { formatVariableName } from '../../utils/variableUtils';
interface RawUpdateFlags {
name?: string;
value?: string;
scope: EASEnvironmentVariableScopeFlagValue;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
visibility?: 'plaintext' | 'sensitive' | 'secret';
type?: 'string' | 'file';
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
}
@@ -54,11 +54,11 @@ interface UpdateFlags {
name?: string;
value?: string;
scope: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
visibility?: 'plaintext' | 'sensitive' | 'secret';
type?: 'string' | 'file';
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
}
@@ -69,7 +69,7 @@ export default class EnvUpdate extends EasCommand {
'variable-name': Flags.string({
description: 'Current name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to update',
}),
@@ -93,7 +93,7 @@ export default class EnvUpdate extends EasCommand {
{
name: 'environment',
description:
"Current environment of the variable to update. One of 'production', 'preview', or 'development'.",
"Current environment of the variable to update. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -180,15 +180,19 @@ export default class EnvUpdate extends EasCommand {
visibility: newVisibility,
type: newType,
fileName,
} = await this.promptForMissingFlagsAsync(selectedVariable, {
name,
value: rawValue,
environment: environments,
visibility,
'non-interactive': nonInteractive,
type,
scope,
});
} = await this.promptForMissingFlagsAsync(
selectedVariable,
{
name,
value: rawValue,
environment: environments,
visibility,
'non-interactive': nonInteractive,
type,
scope,
},
{ graphqlClient, projectId }
);
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: selectedVariable.id,
@@ -226,12 +230,6 @@ export default class EnvUpdate extends EasCommand {
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toUpperCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return {
...flags,
'variable-environment': environment,
@@ -252,7 +250,8 @@ export default class EnvUpdate extends EasCommand {
'non-interactive': nonInteractive,
type,
...rest
}: UpdateFlags
}: UpdateFlags,
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
): Promise<
Omit<UpdateFlags, 'type' | 'visibility'> & {
type?: EnvironmentSecretType;
@@ -316,7 +315,10 @@ export default class EnvUpdate extends EasCommand {
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
canEnterCustomEnvironment: true,
selectedEnvironments: selectedVariable.environments ?? [],
graphqlClient,
projectId,
});
if (
@@ -23,7 +23,6 @@ import {
AppPlatform,
BuildFragment,
BuildStatus,
EnvironmentVariableEnvironment,
FingerprintFragment,
UpdateFragment,
} from '../../graphql/generated';
@@ -97,7 +96,7 @@ export default class FingerprintCompare extends EasCommand {
open: Flags.boolean({
description: 'Open the fingerprint comparison in the browser',
}),
environment: Flags.enum<EnvironmentVariableEnvironment>({
environment: Flags.string({
...EasEnvironmentFlagParameters,
description:
'If generating a fingerprint from the local directory, use the specified environment.',
@@ -15,7 +15,7 @@ import {
getFingerprintInfoFromLocalProjectForPlatformsAsync,
stringToAppPlatform,
} from '../../fingerprint/utils';
import { AppPlatform, EnvironmentVariableEnvironment } from '../../graphql/generated';
import { AppPlatform } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { link } from '../../log';
import { promptAsync } from '../../prompts';
@@ -39,7 +39,7 @@ export default class FingerprintGenerate extends EasCommand {
options: ['android', 'ios'],
}),
...EASEnvironmentFlag,
environment: Flags.enum<EnvironmentVariableEnvironment>({
environment: Flags.string({
...EasEnvironmentFlagParameters,
exclusive: ['build-profile'],
}),
@@ -0,0 +1,714 @@
import { ExpoConfig } from '@expo/config';
import fs from 'fs-extra';
import merge from 'ts-deepmerge';
import { getEASUpdateURL } from '../../../api';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { jester, jester2 } from '../../../credentials/__tests__/fixtures-constants';
import { AppFragment, Role } from '../../../graphql/generated';
import { AppMutation } from '../../../graphql/mutations/AppMutation';
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git';
import {
installDependenciesAsync,
promptForPackageManagerAsync,
} from '../../../onboarding/installDependencies';
import { runCommandAsync } from '../../../onboarding/runCommand';
import { Ora, ora } from '../../../ora';
import {
createOrModifyExpoConfigAsync,
getPrivateExpoConfigAsync,
} from '../../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { promptAsync } from '../../../prompts';
import { Actor, getActorUsername } from '../../../user/User';
import New, {
cloneTemplateAsync,
copyProjectTemplatesAsync,
createProjectAsync,
formatScriptCommand,
generateAppConfigAsync,
generateEasConfigAsync,
getAccountChoices,
initializeGitRepositoryAsync,
installProjectDependenciesAsync,
mergeReadmeAsync,
promptForTargetDirectoryAsync,
updatePackageJsonAsync,
} from '../new';
jest.mock('../../../prompts');
jest.mock('../../../onboarding/git');
jest.mock('../../../onboarding/runCommand');
jest.mock('../../../graphql/mutations/AppMutation');
jest.mock('../../../project/expoConfig');
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../../../user/User', () => ({
getActorUsername: jest.fn(),
}));
jest.mock('../../../ora');
jest.mock('fs-extra');
jest.mock('../../../onboarding/installDependencies');
jest.mock('../../../api');
jest.mock('../../../build/utils/url');
jest.mock('../../../utils/easCli', () => ({
easCliVersion: '5.0.0',
}));
// Remove Log module mocking entirely - use the actual module
describe(New.name, () => {
let consoleLogSpy: jest.SpyInstance;
beforeEach(() => {
jest.clearAllMocks();
// Capture console.log calls
consoleLogSpy = jest.spyOn(console, 'log');
jest.mocked(fs.writeFile).mockImplementation(() => Promise.resolve());
jest.mocked(fs.readJson).mockImplementation(() => Promise.resolve({}));
jest.mocked(fs.writeJson).mockImplementation(() => Promise.resolve());
jest.mocked(fs.copy).mockImplementation(() => Promise.resolve());
jest.mocked(fs.remove).mockImplementation(() => Promise.resolve());
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
});
afterEach(() => {
consoleLogSpy.mockRestore();
});
// Helper function to get all console output as strings
const getConsoleOutput = (): string[] => {
return consoleLogSpy.mock.calls.map(call => (call.length === 0 ? '' : call.join(' ')));
};
// Helper function to check if a specific message was logged
const expectConsoleToContain = (message: string): void => {
const output = getConsoleOutput();
// strip out ANSI codes and special characters like the tick
const outputWithoutAnsi = output.map(line =>
line.replace(/\x1b\[[0-9;]*m/g, '').replace(/✔\s*/, '')
);
// throw new Error(message + '\n' + outputWithoutAnsi.join('\n'));
expect(outputWithoutAnsi.some(line => line.includes(message))).toBeTruthy();
};
// Helper function to verify prompt was called with expected message
const expectPromptToHaveMessage = (expectedMessage: string): void => {
expect(promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
message: expectedMessage,
})
);
};
// Helper function to simulate user input via prompt
const mockUserInput = (inputValue: any): void => {
jest.mocked(promptAsync).mockResolvedValue(inputValue);
};
describe('helper functions', () => {
describe('promptForTargetDirectoryAsync', () => {
it('should prompt for target directory', async () => {
const promptedDirectory = '/test/prompted-project';
mockUserInput({ targetProjectDir: promptedDirectory });
const result = await promptForTargetDirectoryAsync();
expectConsoleToContain(
`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`
);
expectPromptToHaveMessage('Where would you like to create your new project directory?');
expect(result).toBe(promptedDirectory);
});
it('should handle the target directory from args', async () => {
const providedDirectory = '/test/provided-project';
const result = await promptForTargetDirectoryAsync(providedDirectory);
expectConsoleToContain(
`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`
);
expect(promptAsync).not.toHaveBeenCalled();
expect(result).toBe(providedDirectory);
});
});
describe('cloneTemplateAsync', () => {
it('should clone the template project with ssh', async () => {
const targetProjectDir = '/test/target-project';
const finalTargetProjectDir = '/test/final-target-project';
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(true);
jest.mocked(runGitCloneAsync).mockResolvedValue({
targetProjectDir: finalTargetProjectDir,
});
const result = await cloneTemplateAsync(targetProjectDir);
expectConsoleToContain(`📂 Cloning the project to ${targetProjectDir}`);
expectConsoleToContain('We detected that ssh is your preferred git clone method');
expect(runGitCloneAsync).toHaveBeenCalledWith({
githubUsername: 'expo',
githubRepositoryName: 'expo-template-default',
targetProjectDir,
cloneMethod: 'ssh',
});
expect(result).toBe(finalTargetProjectDir);
});
it('should clone the template project with https', async () => {
const targetProjectDir = '/test/target-project';
const finalTargetProjectDir = '/test/final-target-project';
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(false);
jest.mocked(runGitCloneAsync).mockResolvedValue({
targetProjectDir: finalTargetProjectDir,
});
const result = await cloneTemplateAsync(targetProjectDir);
expectConsoleToContain(`📂 Cloning the project to ${targetProjectDir}`);
expectConsoleToContain('We detected that https is your preferred git clone method');
expect(runGitCloneAsync).toHaveBeenCalledWith({
githubUsername: 'expo',
githubRepositoryName: 'expo-template-default',
targetProjectDir,
cloneMethod: 'https',
});
expect(result).toBe(finalTargetProjectDir);
});
});
describe('installProjectDependenciesAsync', () => {
it('should install the project dependencies', async () => {
const projectDir = '/test/project-directory';
jest.mocked(promptForPackageManagerAsync).mockResolvedValue('npm');
jest.mocked(installDependenciesAsync).mockResolvedValue();
jest.mocked(runCommandAsync).mockResolvedValue();
const packageManager = await installProjectDependenciesAsync(projectDir);
expect(packageManager).toBe('npm');
expect(promptForPackageManagerAsync).toHaveBeenCalled();
expect(installDependenciesAsync).toHaveBeenCalledWith({
projectDir,
packageManager: 'npm',
});
expect(runCommandAsync).toHaveBeenCalledWith({
cwd: projectDir,
command: 'npx',
args: ['expo', 'install', 'expo-updates'],
});
expect(runCommandAsync).toHaveBeenCalledWith({
cwd: projectDir,
command: 'npx',
args: ['expo', 'install', '@expo/metro-runtime'],
});
});
});
describe('getAccountChoices', () => {
it('should return mapped account choices', () => {
// Use jester fixture which has multiple accounts with different permissions
const namesWithSufficientPermissions = new Set(['jester']); // Only personal account has permissions
const result = getAccountChoices(jester, namesWithSufficientPermissions);
expect(result).toEqual([
{
title: 'jester (personal account)',
value: { name: 'jester' },
},
{
title: 'other',
value: { name: 'other' },
disabled: true,
description:
'You do not have the required permissions to create projects on this account.',
},
]);
});
});
describe('createProjectAsync', () => {
const mockGraphqlClient = {} as ExpoGraphqlClient;
const projectDir = '/test/project-dir';
const mockProjectId = 'test-project-id';
beforeEach(() => {
// use single-account jester for most of the tests
jest.mocked(getActorUsername).mockReturnValue('jester2');
jest.mocked(getPrivateExpoConfigAsync).mockResolvedValue({
name: 'test-app',
slug: 'test-app',
extra: {},
} as ExpoConfig);
jest.mocked(createOrModifyExpoConfigAsync).mockResolvedValue({
type: 'success' as const,
message: 'Config updated',
config: {
name: 'test-app',
slug: 'test-app',
extra: { eas: { projectId: 'test-project-id' } },
} as ExpoConfig,
});
const mockSpinner: Partial<Ora> = {
start: jest.fn().mockReturnThis(),
succeed: jest.fn().mockReturnThis(),
fail: jest.fn().mockReturnThis(),
};
jest.mocked(ora).mockReturnValue(mockSpinner as Ora);
});
it('should create a project', async () => {
const mockActor = jester2 as Actor;
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
jest.mocked(AppMutation.createAppAsync).mockResolvedValue(mockProjectId);
const result = await createProjectAsync(mockGraphqlClient, mockActor, projectDir);
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
mockGraphqlClient,
'jester2',
'jester2-app'
);
expect(AppMutation.createAppAsync).toHaveBeenCalledWith(mockGraphqlClient, {
accountId: 'jester2-account-id',
projectName: 'jester2-app',
});
expect(createOrModifyExpoConfigAsync).toHaveBeenCalledWith(
projectDir,
{
extra: { eas: { projectId: mockProjectId } },
},
{ skipSDKVersionRequirement: true }
);
expect(result).toBe(mockProjectId);
});
it('should create a project when there are multiple accounts', async () => {
// Use jester fixture which has multiple accounts
const mockActor = jester;
jest.mocked(getActorUsername).mockReturnValue('jester');
jest.mocked(promptAsync).mockResolvedValue({
account: { name: 'jester' },
});
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
jest.mocked(AppMutation.createAppAsync).mockResolvedValue(mockProjectId);
const result = await createProjectAsync(mockGraphqlClient, mockActor, projectDir);
expect(promptAsync).toHaveBeenCalledWith({
type: 'select',
name: 'account',
message: 'Which account should own this project?',
choices: expect.any(Array),
});
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
mockGraphqlClient,
'jester',
'jester-app'
);
expect(AppMutation.createAppAsync).toHaveBeenCalledWith(mockGraphqlClient, {
accountId: 'jester-account-id',
projectName: 'jester-app',
});
expect(result).toBe(mockProjectId);
});
it('should throw when the project already exists', async () => {
const mockActor = jester2 as Actor;
const existingProjectId = 'existing-project-id';
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockResolvedValue(existingProjectId);
await expect(createProjectAsync(mockGraphqlClient, mockActor, projectDir)).rejects.toThrow(
`Existing project found: @jester2/jester2-app (ID: ${existingProjectId}). Project ID configuration canceled. Re-run the command to select a different account/project.`
);
expect(AppMutation.createAppAsync).not.toHaveBeenCalled();
});
it('should throw when the user does not have permission to create projects on account', async () => {
// Create a custom actor with ViewOnly permissions for this test
const mockActor = merge(jester2, {
accounts: [{ users: [{ actor: { id: 'view-only-jester' }, role: Role.ViewOnly }] }],
}) as Actor;
jest.mocked(getActorUsername).mockReturnValue('view-only-jester');
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
await expect(createProjectAsync(mockGraphqlClient, mockActor, projectDir)).rejects.toThrow(
`You don't have permission to create a new project on the jester account and no matching project already exists on the account.`
);
expect(AppMutation.createAppAsync).not.toHaveBeenCalled();
});
});
describe('generateAppConfigAsync', () => {
const projectDir = '/test/project-dir';
const mockUpdateUrl = 'https://u.expo.dev/test-project-id';
const createMockApp = (overrides: Partial<AppFragment> = {}): AppFragment => ({
id: 'test-project-id',
name: 'Test App',
fullName: '@testowner/test-app',
slug: 'test-app',
ownerAccount: {
id: 'testowner-account-id',
name: 'testowner',
users: [],
},
...overrides,
});
beforeEach(() => {
jest.mocked(getEASUpdateURL).mockReturnValue(mockUpdateUrl);
jest.mocked(fs.readJson).mockResolvedValue({
expo: {} as ExpoConfig,
});
});
it('should generate the app config', async () => {
const mockApp = createMockApp();
jest.mocked(fs.readJson).mockResolvedValue({
expo: {
name: 'value-to-override',
slug: 'value-to-override',
icon: 'value-to-keep',
} as ExpoConfig,
});
await generateAppConfigAsync(projectDir, mockApp);
expect(getEASUpdateURL).toHaveBeenCalledWith('test-project-id', null);
const expectedConfig = {
expo: {
name: 'Test App',
slug: 'test-app',
scheme: 'TestApp',
icon: 'value-to-keep',
extra: {
eas: {
projectId: 'test-project-id',
},
},
owner: 'testowner',
updates: {
url: mockUpdateUrl,
},
runtimeVersion: {
policy: 'appVersion',
},
ios: {
bundleIdentifier: 'com.testowner.testapp',
},
android: {
package: 'com.testowner.testapp',
},
},
};
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/app.json`, expectedConfig, {
spaces: 2,
});
expectConsoleToContain('Generated app.json');
});
it('should handle invalid characters in the bundle identifier', async () => {
const mockApp = createMockApp({
slug: 'test-app-with-dashes',
ownerAccount: {
id: 'test-owner-account-id',
name: 'test-owner',
users: [],
},
});
await generateAppConfigAsync(projectDir, mockApp);
expect(fs.writeJson).toHaveBeenCalledWith(
`${projectDir}/app.json`,
expect.objectContaining({
expo: expect.objectContaining({
ios: expect.objectContaining({
bundleIdentifier: 'com.testowner.testappwithdashes',
}),
}),
}),
{ spaces: 2 }
);
});
it('should handle invalid characters in the owner account name', async () => {
const mockApp = createMockApp({
slug: 'testapp',
ownerAccount: {
id: 'test-owner-123-account-id',
name: 'test-owner-123',
users: [],
},
});
await generateAppConfigAsync(projectDir, mockApp);
expect(fs.writeJson).toHaveBeenCalledWith(
`${projectDir}/app.json`,
expect.objectContaining({
expo: expect.objectContaining({
ios: expect.objectContaining({
bundleIdentifier: 'com.testowner123.testapp',
}),
}),
}),
{ spaces: 2 }
);
});
it('should handle invalid characters in the slug', async () => {
const mockApp = createMockApp({
slug: 'test@app#with$symbols',
fullName: '@testowner/test@app#with$symbols',
});
await generateAppConfigAsync(projectDir, mockApp);
expect(fs.writeJson).toHaveBeenCalledWith(
`${projectDir}/app.json`,
expect.objectContaining({
expo: expect.objectContaining({
ios: expect.objectContaining({
bundleIdentifier: 'com.testowner.testappwithsymbols',
}),
}),
}),
{ spaces: 2 }
);
});
});
describe('generateEasConfigAsync', () => {
const projectDir = '/test/project-dir';
it('should generate the eas config', async () => {
const expectedEasConfig = {
cli: {
version: '>= 5.0.0',
appVersionSource: 'remote',
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
android: { image: 'latest' },
ios: { image: 'latest' },
},
'development-simulator': {
extends: 'development',
ios: { simulator: true },
},
preview: {
distribution: 'internal',
channel: 'main',
android: { image: 'latest' },
ios: { image: 'latest' },
},
production: {
channel: 'production',
autoIncrement: true,
android: { image: 'latest' },
ios: { image: 'latest' },
},
},
submit: {
production: {},
},
};
await generateEasConfigAsync(projectDir);
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/eas.json`, expectedEasConfig, {
spaces: 2,
});
expectConsoleToContain('Generated eas.json');
});
});
describe('updatePackageJsonAsync', () => {
it('should update the package.json', async () => {
jest.mocked(fs.readJson).mockResolvedValue({
name: 'test-app',
version: '1.0.0',
});
const projectDir = '/test/project-dir';
await updatePackageJsonAsync(projectDir);
const expectedPackageJson = {
name: 'test-app',
version: '1.0.0',
scripts: {
preview: 'npx eas-cli@latest workflow:run publish-preview-update.yml',
'development-builds': 'npx eas-cli@latest workflow:run create-development-builds.yml',
deploy: 'npx eas-cli@latest workflow:run deploy-to-production.yml',
},
};
expect(fs.writeJson).toHaveBeenCalledWith(
`${projectDir}/package.json`,
expectedPackageJson,
{ spaces: 2 }
);
expectConsoleToContain('Updated package.json with scripts');
});
});
describe('copyProjectTemplatesAsync', () => {
it('should copy the project templates', async () => {
const projectDir = '/test/project-dir';
await copyProjectTemplatesAsync(projectDir);
expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('templates/.eas/workflows'),
expect.stringContaining('.eas/workflows'),
{ errorOnExist: false, overwrite: true }
);
expectConsoleToContain('Created EAS workflow files');
});
});
describe('mergeReadmeAsync', () => {
const projectDir = '/test/project-dir';
it('should merge readme when target section is found', async () => {
const existingReadme = `# My App
This is my app.
## Get a fresh project
Some content here.`;
const readmeAdditions = `## Workflows
This project uses EAS Workflows.`;
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(readmeAdditions);
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(existingReadme);
await mergeReadmeAsync(projectDir);
const expectedMergedReadme = `# My App
This is my app.
## Workflows
This project uses EAS Workflows.
## Get a fresh project
Some content here.`;
expect(fs.writeFile).toHaveBeenCalledWith(`${projectDir}/README.md`, expectedMergedReadme);
expectConsoleToContain('Updated README.md with EAS configuration details');
});
it('should append readme when target section is not found', async () => {
const existingReadme = `# My App
This is my app.`;
const readmeAdditions = `## Workflows
This project uses EAS Workflows.`;
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(readmeAdditions);
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(existingReadme);
await mergeReadmeAsync(projectDir);
const expectedMergedReadme = `# My App
This is my app.
## Workflows
This project uses EAS Workflows.
`;
expect(fs.writeFile).toHaveBeenCalledWith(`${projectDir}/README.md`, expectedMergedReadme);
expectConsoleToContain('Updated README.md with EAS configuration details');
});
});
describe('initializeGitRepositoryAsync', () => {
it('should initialize git repository', async () => {
const projectDir = '/test/project-dir';
await initializeGitRepositoryAsync(projectDir);
expect(fs.remove).toHaveBeenCalledWith(`${projectDir}/.git`);
expect(runCommandAsync).toHaveBeenCalledWith({
command: 'git',
args: ['init'],
cwd: projectDir,
});
expect(runCommandAsync).toHaveBeenCalledWith({
command: 'git',
args: ['add', '.'],
cwd: projectDir,
});
expect(runCommandAsync).toHaveBeenCalledWith({
command: 'git',
args: ['commit', '-m', 'Initial commit'],
cwd: projectDir,
});
});
});
describe('formatScriptCommand', () => {
it('should format script commands for package managers', () => {
expect(formatScriptCommand('start', 'npm')).toBe('npm run start');
expect(formatScriptCommand('start', 'yarn')).toBe('yarn start');
expect(formatScriptCommand('start', 'pnpm')).toBe('pnpm start');
});
});
});
});
@@ -0,0 +1,472 @@
import { ExpoConfig } from '@expo/config';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import path from 'path';
import merge from 'ts-deepmerge';
import { getEASUpdateURL } from '../../api';
import { getProjectDashboardUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppFragment, Role } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { learnMore, link } from '../../log';
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboarding/git';
import {
PackageManager,
installDependenciesAsync,
promptForPackageManagerAsync,
} from '../../onboarding/installDependencies';
import { runCommandAsync } from '../../onboarding/runCommand';
import { ora } from '../../ora';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { Choice, promptAsync } from '../../prompts';
import { Actor, getActorUsername } from '../../user/User';
import { easCliVersion } from '../../utils/easCli';
export async function promptForTargetDirectoryAsync(
targetProjectDirFromArgs?: string
): Promise<string> {
Log.log(
`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`
);
Log.newLine();
if (targetProjectDirFromArgs) {
return targetProjectDirFromArgs;
}
const result = await promptAsync({
type: 'text',
name: 'targetProjectDir',
message: 'Where would you like to create your new project directory?',
initial: path.join(process.cwd(), 'new-expo-project'),
});
return result.targetProjectDir;
}
export async function cloneTemplateAsync(targetProjectDir: string): Promise<string> {
const githubUsername = 'expo';
const githubRepositoryName = 'expo-template-default';
Log.log(`📂 Cloning the project to ${targetProjectDir}`);
Log.newLine();
const cloneMethod = (await canAccessRepositoryUsingSshAsync({
githubUsername,
githubRepositoryName,
}))
? 'ssh'
: 'https';
Log.log(chalk.dim(`We detected that ${cloneMethod} is your preferred git clone method`));
Log.newLine();
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
githubUsername,
githubRepositoryName,
targetProjectDir,
cloneMethod,
});
return finalTargetProjectDirectory;
}
export async function installProjectDependenciesAsync(projectDir: string): Promise<PackageManager> {
const packageManager = await promptForPackageManagerAsync();
await installDependenciesAsync({
projectDir,
packageManager,
});
const dependencies = ['expo-updates', '@expo/metro-runtime'];
for (const dependency of dependencies) {
await runCommandAsync({
cwd: projectDir,
command: 'npx',
args: ['expo', 'install', dependency],
});
}
return packageManager;
}
export function getAccountChoices(
actor: Actor,
namesWithSufficientPermissions: Set<string>
): Choice[] {
const sortedAccounts = actor.accounts.sort((a, _b) =>
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
);
return sortedAccounts.map(account => {
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
const accountDisplayName = isPersonalAccount
? `${account.name} (personal account)`
: account.name;
const disabled = !namesWithSufficientPermissions.has(account.name);
return {
title: accountDisplayName,
value: { name: account.name },
...(disabled && {
disabled: true,
description: 'You do not have the required permissions to create projects on this account.',
}),
};
});
}
export async function createProjectAsync(
graphqlClient: ExpoGraphqlClient,
actor: Actor,
projectDir: string
): Promise<string> {
const allAccounts = actor.accounts;
const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set(
allAccounts
.filter(a => a.users.find(it => it.actor.id === actor.id)?.role !== Role.ViewOnly)
.map(it => it.name)
);
let accountName = allAccounts[0].name;
if (allAccounts.length > 1) {
const choices = getAccountChoices(
actor,
accountNamesWhereUserHasSufficientPermissionsToCreateApp
);
accountName = (
await promptAsync({
type: 'select',
name: 'account',
message: 'Which account should own this project?',
choices,
})
).account.name;
}
const projectName = getActorUsername(actor) + '-app';
const projectFullName = `@${accountName}/${projectName}`;
const existingProjectIdOnServer = await findProjectIdByAccountNameAndSlugNullableAsync(
graphqlClient,
accountName,
projectName
);
if (existingProjectIdOnServer) {
throw new Error(
`Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Project ID configuration canceled. Re-run the command to select a different account/project.`
);
}
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
throw new Error(
`You don't have permission to create a new project on the ${accountName} account and no matching project already exists on the account.`
);
}
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
const projectLink = link(projectDashboardUrl, { text: projectFullName });
const account = nullthrows(allAccounts.find(a => a.name === accountName));
const spinner = ora(`Creating ${chalk.bold(projectFullName)}`).start();
let projectId: string;
try {
projectId = await AppMutation.createAppAsync(graphqlClient, {
accountId: account.id,
projectName,
});
spinner.succeed(`Created ${chalk.bold(projectLink)}`);
} catch (err) {
spinner.fail();
throw err;
}
const exp = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true });
await createOrModifyExpoConfigAsync(
projectDir,
{
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
},
{ skipSDKVersionRequirement: true }
);
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
return projectId;
}
export function stripInvalidCharactersForBundleIdentifier(string: string): string {
return string.replaceAll(/[^A-Za-z0-9]/g, '');
}
export async function generateConfigFilesAsync(
projectDir: string,
app: AppFragment
): Promise<void> {
await generateAppConfigAsync(projectDir, app);
await generateEasConfigAsync(projectDir);
await updatePackageJsonAsync(projectDir);
await copyProjectTemplatesAsync(projectDir);
await mergeReadmeAsync(projectDir);
}
export async function generateAppConfigAsync(projectDir: string, app: AppFragment): Promise<void> {
// Android package name requires each component to start with a lowercase letter.
const isUsernameValidSegment = /^[^a-z]/.test(app.ownerAccount.name);
const userPrefix = isUsernameValidSegment ? 'user' : '';
const isSlugValidSegment = /^[^a-z]/.test(app.slug);
const slugPrefix = isSlugValidSegment ? 'app' : '';
const bundleIdentifier = `com.${userPrefix}${stripInvalidCharactersForBundleIdentifier(
app.ownerAccount.name
)}.${slugPrefix}${stripInvalidCharactersForBundleIdentifier(app.slug)}`;
const updateUrl = getEASUpdateURL(app.id, /* manifestHostOverride */ null);
const { expo: baseExpoConfig } = await fs.readJson(path.join(projectDir, 'app.json'));
const expoConfig: ExpoConfig = {
name: app.name ?? app.slug,
slug: app.slug,
scheme: stripInvalidCharactersForBundleIdentifier(app.name ?? app.slug),
extra: {
eas: {
projectId: app.id,
},
},
owner: app.ownerAccount.name,
updates: {
url: updateUrl,
},
runtimeVersion: {
policy: 'appVersion',
},
ios: {
bundleIdentifier,
},
android: {
package: bundleIdentifier,
},
};
const mergedConfig = merge(baseExpoConfig, expoConfig);
const appJsonPath = path.join(projectDir, 'app.json');
await fs.writeJson(appJsonPath, { expo: mergedConfig }, { spaces: 2 });
Log.withTick(
`Generated ${chalk.bold('app.json')}. ${learnMore(
'https://docs.expo.dev/versions/latest/config/app/'
)}`
);
Log.log();
}
export async function generateEasConfigAsync(projectDir: string): Promise<void> {
const easBuildGitHubConfig = {
android: {
image: 'latest',
},
ios: {
image: 'latest',
},
};
const easJson: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
appVersionSource: AppVersionSource.REMOTE,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
...easBuildGitHubConfig,
},
'development-simulator': {
extends: 'development',
ios: {
simulator: true,
},
},
preview: {
distribution: 'internal',
channel: 'main',
...easBuildGitHubConfig,
},
production: {
channel: 'production',
autoIncrement: true,
...easBuildGitHubConfig,
},
},
submit: {
production: {},
},
};
const easJsonPath = path.join(projectDir, 'eas.json');
await fs.writeJson(easJsonPath, easJson, { spaces: 2 });
Log.withTick(
`Generated ${chalk.bold('eas.json')}. ${learnMore(
'https://docs.expo.dev/build-reference/eas-json/'
)}`
);
Log.log();
}
export async function updatePackageJsonAsync(projectDir: string): Promise<void> {
const packageJsonPath = path.join(projectDir, 'package.json');
const packageJson = await fs.readJson(packageJsonPath);
if (!packageJson.scripts) {
packageJson.scripts = {};
}
packageJson.scripts.preview = 'npx eas-cli@latest workflow:run publish-preview-update.yml';
packageJson.scripts['development-builds'] =
'npx eas-cli@latest workflow:run create-development-builds.yml';
packageJson.scripts.deploy = 'npx eas-cli@latest workflow:run deploy-to-production.yml';
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
Log.withTick('Updated package.json with scripts');
Log.log();
}
export async function copyProjectTemplatesAsync(projectDir: string): Promise<void> {
const templatesSourceDir = path.join(__dirname, 'templates', '.eas', 'workflows');
const easWorkflowsTargetDir = path.join(projectDir, '.eas', 'workflows');
await fs.copy(templatesSourceDir, easWorkflowsTargetDir, {
overwrite: true,
errorOnExist: false,
});
Log.withTick('Created EAS workflow files');
Log.log();
}
export async function mergeReadmeAsync(projectDir: string): Promise<void> {
const readmeTemplatePath = path.join(__dirname, 'templates', 'readme-additions.md');
const projectReadmePath = path.join(projectDir, 'README.md');
const readmeAdditions = await fs.readFile(readmeTemplatePath, 'utf8');
const existingReadme = await fs.readFile(projectReadmePath, 'utf8');
const targetSection = '## Get a fresh project';
const sectionIndex = existingReadme.indexOf(targetSection);
let mergedReadme: string;
if (sectionIndex !== -1) {
// Insert before "## Get a fresh project" section
const beforeSection = existingReadme.substring(0, sectionIndex).trim();
const afterSection = existingReadme.substring(sectionIndex);
mergedReadme = beforeSection + '\n\n' + readmeAdditions.trim() + '\n\n' + afterSection;
} else {
// Append to the end if section doesn't exist
mergedReadme = existingReadme.trim() + '\n\n' + readmeAdditions.trim() + '\n';
}
await fs.writeFile(projectReadmePath, mergedReadme);
Log.withTick('Updated README.md with EAS configuration details');
Log.log();
}
export async function initializeGitRepositoryAsync(projectDir: string): Promise<void> {
await fs.remove(path.join(projectDir, '.git'));
const commands = [['init'], ['add', '.'], ['commit', '-m', 'Initial commit']];
for (const args of commands) {
await runCommandAsync({
cwd: projectDir,
command: 'git',
args,
});
Log.log();
}
}
export const formatScriptCommand = (script: string, packageManager: PackageManager): string => {
if (packageManager === 'npm') {
return `npm run ${script}`;
}
return `${packageManager} ${script}`;
};
export default class New extends EasCommand {
static override aliases = ['new'];
static override description = "create a new project set up with Expo's services.";
static override flags = {};
static override hidden = true;
static override args = [{ name: 'TARGET_PROJECT_DIRECTORY' }];
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { args } = await this.parse(New);
const {
loggedIn: { actor, graphqlClient },
} = await this.getContextAsync(New, { nonInteractive: false });
if (actor.__typename === 'Robot') {
throw new Error(
'This command is not available for robot users. Make sure you are not using a robot token and try again.'
);
}
Log.warn(
'This command is not yet implemented. It will create a new project, but it will not be fully configured.'
);
Log.log(`👋 Welcome to Expo, ${actor.username}!`);
Log.newLine();
const targetProjectDirectory = await promptForTargetDirectoryAsync(
args.TARGET_PROJECT_DIRECTORY
);
const projectDirectory = await cloneTemplateAsync(targetProjectDirectory);
const packageManager = await installProjectDependenciesAsync(projectDirectory);
const projectId = await createProjectAsync(graphqlClient, actor, projectDirectory);
const app = await AppQuery.byIdAsync(graphqlClient, projectId);
await generateConfigFilesAsync(projectDirectory, app);
await initializeGitRepositoryAsync(projectDirectory);
Log.log('🎉 We finished creating your new project.');
Log.log('Next steps:');
Log.withInfo(`Run \`cd ${projectDirectory}\` to navigate to your project.`);
Log.withInfo(
`Run \`${formatScriptCommand(
'preview',
packageManager
)}\` to create a preview build on EAS. ${learnMore(
'https://docs.expo.dev/eas/workflows/examples/publish-preview-update/'
)}`
);
Log.withInfo(
`Run \`${formatScriptCommand(
'start',
packageManager
)}\` to start developing locally. ${learnMore(
'https://docs.expo.dev/get-started/start-developing/'
)}`
);
Log.withInfo(`See the README.md for more information about your project.`);
Log.newLine();
}
}
@@ -152,7 +152,7 @@ export default class Onboarding extends EasCommand {
}))
? 'ssh'
: 'https';
Log.log(chalk.dim(`We detected that ${cloneMethod} is your preffered git clone method`));
Log.log(chalk.dim(`We detected that ${cloneMethod} is your preferred git clone method`));
Log.log();
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
@@ -0,0 +1,21 @@
name: Create development builds
jobs:
android_development_build:
name: Build Android
type: build
params:
platform: android
profile: development
ios_device_development_build:
name: Build iOS device
type: build
params:
platform: ios
profile: development
ios_simulator_development_build:
name: Build iOS simulator
type: build
params:
platform: ios
profile: development-simulator
@@ -0,0 +1,68 @@
name: Deploy to production
on:
push:
branches: ['main']
jobs:
fingerprint:
name: Fingerprint
type: fingerprint
get_android_build:
name: Check for existing android build
needs: [fingerprint]
type: get-build
params:
fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
profile: production
get_ios_build:
name: Check for existing ios build
needs: [fingerprint]
type: get-build
params:
fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
profile: production
build_android:
name: Build Android
needs: [get_android_build]
if: ${{ !needs.get_android_build.outputs.build_id }}
type: build
params:
platform: android
profile: production
build_ios:
name: Build iOS
needs: [get_ios_build]
if: ${{ !needs.get_ios_build.outputs.build_id }}
type: build
params:
platform: ios
profile: production
submit_android_build:
name: Submit Android Build
needs: [build_android]
type: submit
params:
build_id: ${{ needs.build_android.outputs.build_id }}
submit_ios_build:
name: Submit iOS Build
needs: [build_ios]
type: submit
params:
build_id: ${{ needs.build_ios.outputs.build_id }}
publish_android_update:
name: Publish Android update
needs: [get_android_build]
if: ${{ needs.get_android_build.outputs.build_id }}
type: update
params:
branch: production
platform: android
publish_ios_update:
name: Publish iOS update
needs: [get_ios_build]
if: ${{ needs.get_ios_build.outputs.build_id }}
type: update
params:
branch: production
platform: ios
@@ -0,0 +1,12 @@
name: Publish preview update
on:
push:
branches: ['*']
jobs:
publish_preview_update:
name: Publish preview update
type: update
params:
branch: ${{ github.ref_name || 'test' }}
@@ -0,0 +1,19 @@
## Workflows
This project is configured to use [EAS Workflows](https://docs.expo.dev/eas/workflows/get-started/) to automate some development and release processes. These commands are set up in [`package.json`](./package.json) and can be run using NPM scripts in your terminal.
### Previews
Run `npm run preview` to [publish a preview update](https://docs.expo.dev/eas/workflows/examples/publish-preview-update/) of your project, which can be viewed in Expo Go or in a development build.
### Development Builds
Run `npm run development-builds` to [create a development build](https://docs.expo.dev/eas/workflows/examples/create-development-builds/). Note - you'll need to follow the [Prerequisites](https://docs.expo.dev/eas/workflows/examples/create-development-builds/#prerequisites) to ensure you have the correct emulator setup on your machine.
### Production Deployments
Run `npm run deploy` to [deploy to production](https://docs.expo.dev/eas/workflows/examples/deploy-to-production/). Note - you'll need to follow the [Prerequisites](https://docs.expo.dev/eas/workflows/examples/deploy-to-production/#prerequisites) to ensure you're set up to submit to the Apple and Google stores.
## Hosting
Expo offers hosting for websites and API functions via EAS Hosting. See the [Getting Started](https://docs.expo.dev/eas/hosting/get-started/) guide to learn more.
@@ -10,7 +10,6 @@ import {
AndroidSubmissionConfigInput,
IosSubmissionConfigInput,
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
} from '../../graphql/generated';
import { AppStoreConnectApiKeyQuery } from '../../graphql/queries/AppStoreConnectApiKeyQuery';
import { GoogleServiceAccountKeyQuery } from '../../graphql/queries/GoogleServiceAccountKeyQuery';
@@ -141,12 +140,11 @@ export default class SubmitInternal extends EasCommand {
graphqlClient,
})
);
const track = graphQlTrackToConfigTrack[androidConfig.track] ?? androidConfig.track;
const configInput: z.input<typeof SubmissionConfig.Android.SchemaZ> = {
changesNotSentForReview,
googleServiceAccountKeyJson,
track,
track: androidConfig.track,
...(releaseStatus === SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS
? {
releaseStatus: SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS,
@@ -217,10 +215,3 @@ const graphQlReleaseStatusToConfigReleaseStatus = {
[SubmissionAndroidReleaseStatus.Completed]: SubmissionConfig.Android.ReleaseStatus.COMPLETED,
[SubmissionAndroidReleaseStatus.Halted]: SubmissionConfig.Android.ReleaseStatus.HALTED,
};
const graphQlTrackToConfigTrack = {
[SubmissionAndroidTrack.Production]: SubmissionConfig.Android.ReleaseTrack.PRODUCTION,
[SubmissionAndroidTrack.Beta]: SubmissionConfig.Android.ReleaseTrack.BETA,
[SubmissionAndroidTrack.Alpha]: SubmissionConfig.Android.ReleaseTrack.ALPHA,
[SubmissionAndroidTrack.Internal]: SubmissionConfig.Android.ReleaseTrack.INTERNAL,
};
@@ -39,7 +39,7 @@ export default class UpdateConfigure extends EasCommand {
vcsClient,
} = await this.getContextAsync(UpdateConfigure, {
nonInteractive: flags['non-interactive'],
withServerSideEnvironment: flags['environment'],
withServerSideEnvironment: flags['environment'] ?? null,
});
Log.log(
+59 -60
View File
@@ -4,9 +4,8 @@ import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { getExpoWebsiteBaseUrl } from '../../api';
// import { getExpoWebsiteBaseUrl } from '../../api';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { transformFingerprintSource } from '../../build/graphql';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
@@ -15,7 +14,6 @@ import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
AssetMapSourceInput,
EnvironmentVariableEnvironment,
FingerprintInfoGroup as GraphqlFingerprintInfoGroup,
PublishUpdateGroupInput,
StatuspageServiceName,
@@ -24,7 +22,7 @@ import {
UpdateRolloutInfoGroup,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
// import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
@@ -38,7 +36,7 @@ import {
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
filterCollectedAssetsByRequestedPlatforms,
findCompatibleBuildsAsync,
// findCompatibleBuildsAsync,
generateEasMetadataAsync,
getBranchNameForCommandAsync,
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
@@ -80,7 +78,7 @@ type RawUpdateFlags = {
'rollout-percentage'?: number;
'non-interactive': boolean;
json: boolean;
environment: EnvironmentVariableEnvironment | null;
environment?: string;
};
type UpdateFlags = {
@@ -97,7 +95,7 @@ type UpdateFlags = {
rolloutPercentage?: number;
json: boolean;
nonInteractive: boolean;
environment: EnvironmentVariableEnvironment | null;
environment?: string;
};
export default class UpdatePublish extends EasCommand {
@@ -194,7 +192,7 @@ export default class UpdatePublish extends EasCommand {
getServerSideEnvironmentVariablesAsync,
} = await this.getContextAsync(UpdatePublish, {
nonInteractive,
withServerSideEnvironment: environment,
withServerSideEnvironment: environment ?? null,
});
if (jsonFlag) {
@@ -488,10 +486,7 @@ export default class UpdatePublish extends EasCommand {
(prev, [platform, fingerprintInfo]) => {
return {
...prev,
[platform]: {
...fingerprintInfo,
fingerprintSource: transformFingerprintSource(fingerprintInfo.fingerprintSource),
},
[platform]: fingerprintInfo,
};
},
{} as GraphqlFingerprintInfoGroup
@@ -594,11 +589,11 @@ export default class UpdatePublish extends EasCommand {
Log.addNewLineIfNone();
const runtimeToCompatibleBuilds = await Promise.all(
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
findCompatibleBuildsAsync(graphqlClient, projectId, obj)
)
);
// const runtimeToCompatibleBuilds = await Promise.all(
// runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
// findCompatibleBuildsAsync(graphqlClient, projectId, obj)
// )
// );
for (const runtime of uniqBy(
runtimeToPlatformsAndFingerprintInfoMapping,
@@ -672,52 +667,56 @@ export default class UpdatePublish extends EasCommand {
Log.addNewLineIfNone();
}
const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
)?.fingerprintInfoGroupWithCompatibleBuilds;
if (fingerprintsWithoutCompatibleBuilds) {
const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
([_platform, fingerprintInfo]) => !fingerprintInfo.build
);
if (missingBuilds.length > 0) {
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
Log.warn('No compatible builds found for the following fingerprints:');
for (const [platform, fingerprintInfo] of missingBuilds) {
const fingerprintUrl = new URL(
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
getExpoWebsiteBaseUrl()
);
Log.warn(
formatFields(
[
{
label: `${this.prettyPlatform(platform)} fingerprint`,
value: fingerprintInfo.fingerprintHash,
},
{ label: 'URL', value: fingerprintUrl.toString() },
],
{
labelFormat: label => ` ${chalk.dim(label)}:`,
}
)
);
Log.addNewLineIfNone();
}
}
}
// NOTE(brentvatne): temporarily disable logging this until we can revisit the formatting
// and the logic for it - it's a bit too aggressive right now, and warns even if you're
// not using EAS Build
//
// const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
// ({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
// )?.fingerprintInfoGroupWithCompatibleBuilds;
// if (fingerprintsWithoutCompatibleBuilds) {
// const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
// ([_platform, fingerprintInfo]) => !fingerprintInfo.build
// );
// if (missingBuilds.length > 0) {
// const project = await AppQuery.byIdAsync(graphqlClient, projectId);
// Log.warn('No compatible builds found for the following fingerprints:');
// for (const [platform, fingerprintInfo] of missingBuilds) {
// const fingerprintUrl = new URL(
// `/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
// getExpoWebsiteBaseUrl()
// );
// Log.warn(
// formatFields(
// [
// {
// label: `${this.prettyPlatform(platform)} fingerprint`,
// value: fingerprintInfo.fingerprintHash,
// },
// { label: 'URL', value: fingerprintUrl.toString() },
// ],
// {
// labelFormat: label => ` ${chalk.dim(label)}:`,
// }
// )
// );
// Log.addNewLineIfNone();
// }
// }
// }
}
}
private prettyPlatform(updatePlatform: string): string {
switch (updatePlatform) {
case 'android':
return 'Android';
case 'ios':
return 'iOS';
default:
return updatePlatform;
}
}
// private prettyPlatform(updatePlatform: string): string {
// switch (updatePlatform) {
// case 'android':
// return 'Android';
// case 'ios':
// return 'iOS';
// default:
// return updatePlatform;
// }
// }
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
+184 -75
View File
@@ -1,28 +1,31 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs/promises';
import fsExtra from 'fs-extra';
import path from 'path';
import prompts from 'prompts';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import {
runBuildConfigureIfNeededAsync,
runUpdateConfigureIfNeededAsync,
} from '../../commandUtils/workflow/buildProfileUtils';
import {
WorkflowStarter,
WorkflowStarterName,
customizeTemplateIfNeededAsync,
howToRunWorkflow,
workflowStarters,
} from '../../commandUtils/workflow/creation';
import {
logWorkflowValidationErrors,
validateWorkflowFileAsync,
workflowContentsFromParsedYaml,
} from '../../commandUtils/workflow/validation';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { WorkflowFile } from '../../utils/workflowFile';
const DEFAULT_WORKFLOW_NAME = 'workflow.yml';
const HELLO_WORLD_TEMPLATE = `name: Hello World
on:
push:
branches: ['*']
jobs:
hello_world:
steps:
- uses: eas/checkout
- run: echo "Hello, World"
`;
export class WorkflowCreate extends EasCommand {
static override description = 'create a new workflow configuration YAML file';
@@ -35,11 +38,16 @@ export class WorkflowCreate extends EasCommand {
];
static override flags = {
...EASNonInteractiveFlag,
'skip-validation': Flags.boolean({
description: 'If set, the workflow file will not be validated before being created',
default: false,
}),
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
@@ -48,74 +56,175 @@ export class WorkflowCreate extends EasCommand {
flags,
} = await this.parse(WorkflowCreate);
const { projectDir } = await this.getContextAsync(WorkflowCreate, {
nonInteractive: flags['non-interactive'],
});
try {
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
projectDir,
} = await this.getContextAsync(WorkflowCreate, {
nonInteractive: false,
withServerSideEnvironment: null,
});
let fileName = argFileName;
const { exp: originalExpoConfig, projectId } = await getDynamicPrivateProjectConfigAsync();
let expoConfig = originalExpoConfig;
if (!fileName) {
const response = await prompts({
let workflowStarter;
while (!workflowStarter) {
workflowStarter = await chooseTemplateAsync();
switch (workflowStarter.name) {
case WorkflowStarterName.BUILD:
case WorkflowStarterName.DEPLOY:
case WorkflowStarterName.UPDATE: {
const shouldProceed = await runBuildConfigureIfNeededAsync({
projectDir,
expoConfig,
});
if (!shouldProceed) {
workflowStarter = undefined;
continue;
}
break;
}
default:
break;
}
switch (workflowStarter.name) {
case WorkflowStarterName.DEPLOY:
case WorkflowStarterName.UPDATE: {
const shouldProceed = await runUpdateConfigureIfNeededAsync({
projectDir,
expoConfig,
});
if (!shouldProceed) {
workflowStarter = undefined;
continue;
}
// Need to refetch the Expo config because it may have changed
expoConfig = (await getDynamicPrivateProjectConfigAsync()).exp;
break;
}
default:
break;
}
}
const { fileName, filePath } = await chooseFileNameAsync(
argFileName,
projectDir,
workflowStarter
);
// Customize the template if needed
workflowStarter = await customizeTemplateIfNeededAsync(
workflowStarter,
projectDir,
expoConfig
);
Log.debug(`Creating workflow file ${fileName} from template ${workflowStarter.name}`);
const yamlString = [
workflowStarter.header,
workflowContentsFromParsedYaml(workflowStarter.template),
].join('\n');
if (!flags['skip-validation']) {
await validateWorkflowFileAsync(
{ yamlConfig: yamlString, filePath: fileName },
projectDir,
graphqlClient,
projectId
);
}
await ensureWorkflowsDirectoryExistsAsync({ projectDir });
filePath && (await fs.writeFile(filePath, yamlString));
Log.withTick(`Created ${chalk.bold(filePath)}`);
Log.addNewLineIfNone();
Log.log(howToRunWorkflow(fileName, workflowStarter));
// Next steps
if (workflowStarter.nextSteps && workflowStarter.nextSteps.length > 0) {
Log.addNewLineIfNone();
Log.log('Next steps:');
Log.addNewLineIfNone();
Log.log(
formatFields(
workflowStarter.nextSteps.map((step: string, index: number) => ({
label: `${index + 1}.`,
value: step,
}))
)
);
}
} catch (error) {
logWorkflowValidationErrors(error);
Log.error('Failed to create workflow file.');
}
}
}
async function ensureWorkflowsDirectoryExistsAsync({
projectDir,
}: {
projectDir: string;
}): Promise<void> {
try {
await fs.access(path.join(projectDir, '.eas', 'workflows'));
} catch {
await fs.mkdir(path.join(projectDir, '.eas', 'workflows'), { recursive: true });
Log.withTick(`Created directory ${chalk.bold(path.join(projectDir, '.eas', 'workflows'))}`);
}
}
async function chooseTemplateAsync(): Promise<WorkflowStarter> {
const workflowStarter = (
await promptAsync({
type: 'select',
name: 'starter',
message: 'Select a workflow template:',
choices: workflowStarters.map(starter => ({
title: starter.displayName,
value: starter,
})),
})
).starter;
return workflowStarter;
}
async function chooseFileNameAsync(
initialValue: string | undefined,
projectDir: string,
workflowStarter: WorkflowStarter
): Promise<{ fileName: string; filePath: string }> {
let fileName = initialValue;
let filePath = '';
while ((fileName?.length ?? 0) === 0) {
fileName = (
await promptAsync({
type: 'text',
name: 'fileName',
message: 'What would you like to name your workflow file?',
initial: DEFAULT_WORKFLOW_NAME,
validate: value => {
try {
WorkflowFile.validateYamlExtension(value);
return true;
} catch (error) {
return error instanceof Error ? error.message : 'Invalid file name';
}
},
});
if (!response.fileName) {
Log.warn('Workflow creation cancelled.');
process.exit(0);
}
fileName = response.fileName;
initial: workflowStarter.defaultFileName,
})
).fileName;
if (!fileName) {
fileName = undefined;
continue;
}
try {
await this.ensureWorkflowsDirectoryExistsAsync({ projectDir });
await this.createWorkflowFileAsync({ fileName, projectDir });
WorkflowFile.validateYamlExtension(fileName);
} catch (error) {
Log.error('Failed to create workflow file.');
throw error;
Log.error(error instanceof Error ? error.message : 'Invalid YAML file name extension');
fileName = undefined;
continue;
}
}
private async ensureWorkflowsDirectoryExistsAsync({
projectDir,
}: {
projectDir: string;
}): Promise<void> {
try {
await fs.access(path.join(projectDir, '.eas', 'workflows'));
} catch {
await fs.mkdir(path.join(projectDir, '.eas', 'workflows'), { recursive: true });
Log.withTick(`Created directory ${chalk.bold(path.join(projectDir, '.eas', 'workflows'))}`);
}
}
private async createWorkflowFileAsync({
fileName,
projectDir,
}: {
fileName: string;
projectDir: string;
}): Promise<void> {
WorkflowFile.validateYamlExtension(fileName);
const filePath = path.join(projectDir, '.eas', 'workflows', fileName);
filePath = path.join(projectDir, '.eas', 'workflows', fileName);
if (await fsExtra.pathExists(filePath)) {
throw new Error(`Workflow file already exists: ${filePath}`);
Log.error(`Workflow file already exists: ${filePath}`);
Log.error('Please choose a different file name.');
Log.newLine();
fileName = undefined;
}
await fs.writeFile(filePath, HELLO_WORLD_TEMPLATE);
Log.withTick(`Created ${chalk.bold(filePath)}`);
}
return { fileName: fileName ?? '', filePath };
}
+174 -106
View File
@@ -40,6 +40,7 @@
* ```
*/
import spawnAsync from '@expo/spawn-async';
import { Flags } from '@oclif/core';
import { CombinedError } from '@urql/core';
import chalk from 'chalk';
@@ -55,6 +56,7 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
import {
WorkflowProjectSourceType,
WorkflowRevision,
WorkflowRunByIdQuery,
WorkflowRunStatus,
} from '../../graphql/generated';
@@ -78,7 +80,8 @@ const EXIT_CODES = {
};
export default class WorkflowRun extends EasCommand {
static override description = 'run an EAS workflow';
static override description =
'run an EAS workflow. The entire local project directory will be packaged and uploaded to EAS servers for the workflow run, unless the --ref flag is used.';
static override args = [{ name: 'file', description: 'Path to the workflow file to run' }];
@@ -98,6 +101,11 @@ export default class WorkflowRun extends EasCommand {
'Add a parameter in key=value format. Use multiple instances of this flag to set multiple inputs.',
summary: 'Set workflow inputs',
}),
ref: Flags.string({
description:
"The git reference must exist in the project's git repository, and the workflow file must exist at that reference. When this flag is used, the local project is not uploaded; instead, the workflow is run from the exact state of the project at the chosen reference.",
summary: 'Git reference to run the workflow on',
}),
...EasJsonOnlyFlag,
};
@@ -125,26 +133,70 @@ export default class WorkflowRun extends EasCommand {
withServerSideEnvironment: null,
});
let yamlConfig: string;
try {
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
projectDir,
filePath: args.file,
});
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
yamlConfig = workflowFileContents.yamlConfig;
} catch (err) {
Log.error('Failed to read workflow file.');
throw err;
}
const {
projectId,
exp: { slug: projectName },
} = await getDynamicPrivateProjectConfigAsync();
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
let yamlConfig: string;
let workflowRunId: string;
let workflowRevisionId: string | undefined;
let gitRef: string | undefined;
if (flags.ref) {
// Run from git ref
const fileName = path.basename(args.file);
// Find the real commit, make sure the ref is valid
gitRef = (
await spawnAsync('git', ['rev-parse', flags.ref], {
cwd: projectDir,
})
).output[0].trim();
if (!gitRef) {
throw new Error('Failed to resolve git reference');
}
Log.log(`Using workflow file ${fileName} at ${gitRef}`);
let revisionResult: WorkflowRevision | undefined;
try {
revisionResult = await WorkflowRevisionMutation.getOrCreateWorkflowRevisionFromGitRefAsync(
graphqlClient,
{
appId: projectId,
fileName,
gitRef,
}
);
} catch (err) {
throw new Error(
`Failed to find or create workflow revision for ${fileName} at ${flags.ref}: ${err}`
);
}
Log.debug(`Workflow revision: ${JSON.stringify(revisionResult, null, 2)}`);
if (!revisionResult) {
throw new Error(
`Failed to find or create workflow revision for ${fileName} at ${flags.ref}`
);
}
yamlConfig = revisionResult.yamlConfig;
workflowRevisionId = revisionResult.id;
} else {
// Run from local file
try {
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
projectDir,
filePath: args.file,
});
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
yamlConfig = workflowFileContents.yamlConfig;
} catch (err) {
Log.error('Failed to read workflow file.');
throw err;
}
}
// Validate workflow YAML
try {
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
appId: projectId,
@@ -154,8 +206,6 @@ export default class WorkflowRun extends EasCommand {
if (error instanceof CombinedError) {
WorkflowFile.maybePrintWorkflowFileValidationErrors({
error,
accountName: account.name,
projectName,
});
throw error;
@@ -208,81 +258,95 @@ export default class WorkflowRun extends EasCommand {
path.relative(await vcsClient.getRootPathAsync(), projectDir) || '.'
);
try {
({ projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
graphqlClient,
vcsClient,
accountId: account.id,
}));
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 {
({ id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(graphqlClient, {
appId: projectId,
workflowRevisionInput: {
fileName: path.basename(args.file),
yamlConfig,
},
workflowRunInput: {
if (gitRef) {
// Run from git ref
let runResult: { id: string };
try {
runResult = await WorkflowRunMutation.createWorkflowRunFromGitRefAsync(graphqlClient, {
workflowRevisionId: workflowRevisionId ?? '',
gitRef,
inputs,
projectSource: {
type: WorkflowProjectSourceType.Gcs,
projectArchiveBucketKey,
easJsonBucketKey,
packageJsonBucketKey,
projectRootDirectory,
});
} catch (err) {
throw new Error(`Failed to create workflow run: ${err}`);
}
workflowRunId = runResult.id;
} else {
// Run from local file
try {
({ projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
graphqlClient,
vcsClient,
accountId: account.id,
}));
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;
}
try {
({ id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(graphqlClient, {
appId: projectId,
workflowRevisionInput: {
fileName: path.basename(args.file),
yamlConfig,
},
},
}));
workflowRunInput: {
inputs,
projectSource: {
type: WorkflowProjectSourceType.Gcs,
projectArchiveBucketKey,
easJsonBucketKey,
packageJsonBucketKey,
projectRootDirectory,
},
},
}));
} catch (err) {
Log.error('Failed to start the workflow with the API.');
Log.newLine();
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
} catch (err) {
Log.error('Failed to start the workflow with the API.');
throw err;
throw err;
}
}
Log.newLine();
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput({
@@ -448,24 +512,15 @@ export function parseJsonInputs(jsonString: string): Record<string, string> {
// https://github.com/colinhacks/zod/issues/2985#issuecomment-2230692578
const booleanLike = z.union([
z.boolean(),
z.number().pipe(z.coerce.boolean()),
z.preprocess(val => {
if (typeof val === 'string') {
if (val.toLowerCase() === 'true') {
return true;
}
if (val.toLowerCase() === 'false') {
return false;
}
}
return val;
}, z.boolean()),
z.codec(z.number(), z.boolean(), {
decode: n => !!n,
encode: b => (b ? 1 : 0),
}),
z.stringbool({ truthy: ['true', 'True'], falsy: ['false', 'False'] }),
]);
const stringLike = z
.union([
const stringLike = z.codec(
z.union([
// We're going to coerce numbers and strings into strings.
z.number(),
z.string(),
@@ -475,13 +530,26 @@ const stringLike = z
// and parses both as JS `true` -- if we stringified that,
// we would lose the capital "T" which may not be what the user expects,
// - nulls - user should do `"null"` or not pass the property at all.
])
.pipe(z.coerce.string());
]),
z.string(),
{
decode: value => {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return String(value);
}
throw new Error(`Cannot convert ${typeof value} to string: ${value}`);
},
encode: value => value,
}
);
export const WorkflowDispatchInputZ = z
.object({
description: stringLike.optional().describe('Description of the input'),
required: booleanLike.default(false).describe('Whether the input is required'),
required: booleanLike.default(false).describe('Whether the input is required.'),
})
.and(
z.union([
@@ -1,12 +1,11 @@
import { CombinedError } from '@urql/core';
import * as YAML from 'yaml';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
import {
logWorkflowValidationErrors,
validateWorkflowFileAsync,
} from '../../commandUtils/workflow/validation';
import Log from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { WorkflowFile } from '../../utils/workflowFile';
export class WorkflowValidate extends EasCommand {
@@ -27,6 +26,7 @@ export class WorkflowValidate extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
@@ -36,67 +36,30 @@ export class WorkflowValidate extends EasCommand {
flags,
} = await this.parse(WorkflowValidate);
const {
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
projectDir,
} = await this.getContextAsync(WorkflowValidate, {
nonInteractive: flags['non-interactive'],
withServerSideEnvironment: null,
});
const {
projectId,
exp: { slug: projectName },
} = await getDynamicPrivateProjectConfigAsync();
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const spinner = ora().start('Validating the workflow YAML file…');
try {
const {
loggedIn: { graphqlClient },
projectDir,
projectId,
} = await this.getContextAsync(WorkflowValidate, {
nonInteractive: flags['non-interactive'],
withServerSideEnvironment: null,
});
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
projectDir,
filePath,
});
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
// Check if the parsed result is empty or null
if (
parsedYaml === null ||
parsedYaml === undefined ||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
) {
throw new Error('YAML file is empty or contains only comments.');
}
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
appId: projectId,
yamlConfig: workflowFileContents.yamlConfig,
});
await validateWorkflowFileAsync(workflowFileContents, projectDir, graphqlClient, projectId);
spinner.succeed('Workflow configuration YAML is valid.');
} catch (error) {
spinner.fail('Workflow configuration YAML is not valid.');
if (error instanceof YAML.YAMLParseError) {
Log.error(`YAML syntax error: ${error.message}`);
} else if (error instanceof CombinedError) {
WorkflowFile.maybePrintWorkflowFileValidationErrors({
error,
accountName: account.name,
projectName,
});
throw error;
} else if (error instanceof Error) {
Log.error(error.message);
} else {
Log.error(`Unexpected error: ${String(error)}`);
}
throw error;
logWorkflowValidationErrors(error);
}
}
}
+109 -13
View File
@@ -1,3 +1,6 @@
import chalk from 'chalk';
import { formatGraphQLBuildArtifacts } from '../../build/utils/formatBuild';
import { getWorkflowRunUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
@@ -7,12 +10,52 @@ import {
} from '../../commandUtils/workflow/stateMachine';
import { WorkflowTriggerType } from '../../commandUtils/workflow/types';
import { computeTriggerInfoForWorkflowRun } from '../../commandUtils/workflow/utils';
import { WorkflowRunByIdWithJobsQuery } from '../../graphql/generated';
import {
BuildArtifacts,
WorkflowArtifact,
WorkflowJobType,
WorkflowRunByIdWithJobsQuery,
} from '../../graphql/generated';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log, { link } from '../../log';
import formatFields from '../../utils/formatFields';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
type ReducedWorkflowArtifact = Omit<
WorkflowArtifact,
'createdAt' | 'jobRun' | 'storageType' | 'updatedAt'
>;
type WorkflowJobOutput =
WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number]['outputs'];
type WorkflowJobResult = WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] & {
artifacts?: ReducedWorkflowArtifact[] | BuildArtifacts | undefined;
output?: WorkflowJobOutput | undefined;
};
type WorkflowRunResult = WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] & {
logURL?: string | undefined;
triggerType?: WorkflowTriggerType | undefined;
trigger?: string | null;
jobs: WorkflowJobResult[];
};
const processedOutputs: (job: WorkflowJobResult) => FormatFieldsItem[] = job => {
const result: FormatFieldsItem[] = [];
const keys = job.outputs ? Object.keys(job.outputs) : [];
keys.forEach(key => {
result.push({
label: ` ${key}`,
get value(): string {
const value = job.outputs[key];
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
},
});
});
return result;
};
export default class WorkflowView extends EasCommand {
static override description =
'view details for a workflow run, including jobs. If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.';
@@ -60,11 +103,6 @@ export default class WorkflowView extends EasCommand {
}
const idToQuery = actionResult.runId ?? '';
type WorkflowRunResult = WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] & {
logURL?: string;
triggerType?: WorkflowTriggerType;
trigger?: string | null;
};
const result: WorkflowRunResult = await WorkflowRunQuery.withJobsByIdAsync(
graphqlClient,
idToQuery,
@@ -76,9 +114,18 @@ export default class WorkflowView extends EasCommand {
result.triggerType = triggerType;
result.trigger = trigger;
result.jobs.forEach(job => {
delete job.turtleJobRun;
const processedJobs: WorkflowJobResult[] = result.jobs.map(job => {
const processedJob = job as WorkflowJobResult;
if (job.type === WorkflowJobType.Build) {
processedJob.artifacts = job.turtleBuild?.artifacts ?? undefined;
} else {
processedJob.artifacts = job.turtleJobRun?.artifacts;
}
delete processedJob.turtleJobRun;
return processedJob;
});
result.jobs = processedJobs;
result.logURL = getWorkflowRunUrl(
result.workflow.app.ownerAccount.name,
result.workflow.app.name,
@@ -98,7 +145,7 @@ export default class WorkflowView extends EasCommand {
{ label: 'Trigger', value: result.trigger ?? 'null' },
{
label: 'Git Commit Message',
value: result.gitCommitMessage?.split('\n')[0] ?? null ?? 'null',
value: result.gitCommitMessage?.split('\n')[0] ?? 'null',
},
{ label: 'Status', value: result.status },
{ label: 'Errors', value: result.errors.map(error => error.title).join('\n') },
@@ -108,7 +155,7 @@ export default class WorkflowView extends EasCommand {
])
);
Log.addNewLineIfNone();
result.jobs.forEach(job => {
result.jobs.forEach((job: WorkflowJobResult) => {
Log.log(
formatFields([
{ label: 'Job ID', value: job.id },
@@ -118,10 +165,59 @@ export default class WorkflowView extends EasCommand {
{ label: ' Type', value: job.type },
{ label: ' Created At', value: job.createdAt },
{ label: ' Updated At', value: job.updatedAt },
{ label: ' Outputs', value: JSON.stringify(job.outputs, null, 2) },
{ label: ' Errors', value: job.errors.map(error => error.title).join('\n') },
])
);
if (job.errors.length > 0) {
Log.gray(chalk.dim(' Errors:'));
job.errors.forEach(error => {
Log.log(formatFields([{ label: ` ${error.title}`, value: `${error.message}` }]));
});
}
if (job.outputs) {
const outputs = processedOutputs(job);
if (outputs.length > 0) {
Log.gray(chalk.dim(' Outputs:'));
Log.log(formatFields(outputs));
}
}
if (job.type === WorkflowJobType.Build) {
if (job.turtleBuild?.artifacts) {
Log.gray(chalk.dim(' Artifacts:'));
Log.log(
formatFields(
formatGraphQLBuildArtifacts(job.turtleBuild).map(item => {
item.label = ` ${item.label}`;
return item;
})
)
);
}
} else {
const jobArtifacts = job.artifacts as ReducedWorkflowArtifact[];
if (jobArtifacts?.length) {
Log.gray(chalk.dim(' Artifacts:'));
jobArtifacts.forEach(artifact => {
Log.log(
formatFields([
{ label: ' ID', value: artifact.id },
{ label: ' Name', value: artifact.name },
{ label: ' Content Type', value: artifact?.contentType ?? 'null' },
{
label: ' File Size Bytes',
value: artifact?.fileSizeBytes ? `${artifact.fileSizeBytes}` : 'null',
},
{ label: ' Filename', value: artifact.filename },
{
label: ' Download URL',
value: artifact?.downloadUrl ? link(artifact.downloadUrl) : 'null',
},
])
);
Log.addNewLineIfNone();
});
}
}
Log.addNewLineIfNone();
});
}
@@ -44,6 +44,21 @@ export const jester2 = {
featureGates: {},
};
export const robot: Actor = {
__typename: 'Robot',
id: 'robot-id',
firstName: 'Test Robot',
accounts: [
{
id: 'robot-account-id',
name: 'robot-account',
users: [],
},
],
isExpoAdmin: false,
featureGates: {},
};
assert(jester.__typename === 'User');
export const testUsername = jester.username;
export const testSlug = 'testApp';
@@ -15,6 +15,7 @@ function createMockBundleId(id: string = 'XXX', capabilities: BundleIdCapability
id,
} as any;
}
const ctx = Object.freeze({ providerId: 123195, teamId: 'MyteamId' });
describe(assertValidOptions, () => {
it(`adds a reason for asserting capability identifiers`, () => {
@@ -29,7 +30,6 @@ describe(assertValidOptions, () => {
describe(syncCapabilitiesForEntitlementsAsync, () => {
it(`does not disable associated domains when MDM managed domains is active`, async () => {
const ctx = { providerId: 123195, teamId: 'MyteamId' };
// https://forums.expo.dev/t/eas-build-failed-on-ios-associated-domains-capability/61662/5
const capabilities = [
{
@@ -67,8 +67,46 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
]);
});
it('skips syncing capabilities with settings if they do not contain the expected `enabled` field', async () => {
const capabilityJson = {
id: 'U78L9459DG_APPLE_ID_AUTH',
attributes: {
settings: [
{
key: 'APPLE_ID_AUTH_APP_CONSENT',
options: [
{
key: 'PRIMARY_APP_CONSENT',
},
],
},
],
capabilityType: 'APPLE_ID_AUTH',
},
};
const capability = new BundleIdCapability(
ctx,
capabilityJson.id,
capabilityJson.attributes as any
);
const bundleId = createMockBundleId('U78L9459DG', [capability]);
const result = await syncCapabilitiesForEntitlementsAsync(
bundleId,
{
'com.apple.developer.applesignin': ['Default'],
},
noBroadcastNotificationOption
);
expect(result).toStrictEqual({
enabled: [],
disabled: [],
});
expect(bundleId.updateBundleIdCapabilityAsync).not.toHaveBeenCalled();
});
describe('capabilities with settings', () => {
const ctx = { providerId: 123195, teamId: 'MyteamId' };
const capabilities = [
{
id: 'U78L9459DG_APPLE_ID_AUTH',
@@ -266,7 +304,6 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
});
describe('given a bundleId with a capability that is enabled', () => {
const ctx = { providerId: 123195, teamId: 'MyteamId' };
const remote = {
id: 'UFJ54VZ75A_ACCESS_WIFI_INFORMATION',
attributes: {
@@ -380,7 +417,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
noBroadcastNotificationOption
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
expect(bundleId.updateBundleIdCapabilityAsync).toHaveBeenCalledWith([
{ capabilityType: 'HEALTHKIT', option: 'ON' },
{ capabilityType: 'APPLE_PAY', option: 'ON' },
{ capabilityType: 'ICLOUD', option: 'ON' },
@@ -696,7 +733,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
noBroadcastNotificationOption
);
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
expect(bundleId.updateBundleIdCapabilityAsync).toHaveBeenCalledWith([
{ capabilityType: 'HEALTHKIT', option: 'ON' },
]);
@@ -786,7 +823,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
}
);
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
expect(bundleId.updateBundleIdCapabilityAsync).not.toHaveBeenCalled();
expect(results.disabled).toStrictEqual([]);
expect(results.enabled).toStrictEqual([]);
@@ -807,7 +844,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
noBroadcastNotificationOption
);
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
expect(bundleId.updateBundleIdCapabilityAsync).not.toHaveBeenCalled();
expect(results.disabled).toStrictEqual([]);
expect(results.enabled).toStrictEqual([]);
@@ -129,7 +129,7 @@ function getCapabilitiesToEnable(
const { op } = operation;
if (Log.isDebug) {
Log.log(`Will ${op} remote capability: ${key} (${staticCapabilityInfo.name}.`);
Log.log(`Will ${op} remote capability: ${key} (${staticCapabilityInfo.name}).`);
}
if (op === 'enable') {
enabledCapabilityNames.push(staticCapabilityInfo.name);
@@ -12,6 +12,8 @@ import { JSONObject, JSONValue } from '@expo/json-file';
import { invariant } from 'graphql/jsutils/invariant';
import nullthrows from 'nullthrows';
import Log from '../../../log';
const validateBooleanOptions = (options: any): boolean => {
return typeof options === 'boolean';
};
@@ -86,21 +88,32 @@ const capabilityWithSettingsSyncOperation: GetSyncOperation = ({
if (!existingRemote) {
return enableOp;
}
invariant(
'enabled' in existingRemote.attributes,
`Expected "enabled" attribute in ${existingRemote.id}`
);
const existingEnabled = existingRemote.attributes.enabled === true;
const newOption = entitlementValue ? CapabilityTypeOption.ON : CapabilityTypeOption.OFF;
const { attributes, id } = existingRemote;
if ('enabled' in attributes) {
// the `enabled` field should be available as per https://developer.apple.com/documentation/appstoreconnectapi/capabilitysetting
const existingEnabled = attributes.enabled === true;
// If both are enabled and the existing one has settings, skip the update
if (existingEnabled && entitlementValue && existingRemote.attributes.settings) {
// If both are enabled and the existing one has settings, skip the update
if (existingEnabled && entitlementValue && attributes.settings) {
return skipOp;
}
const newOption = entitlementValue ? CapabilityTypeOption.ON : CapabilityTypeOption.OFF;
// If the states don't match, we need to update
const newEnabled = newOption === CapabilityTypeOption.ON;
return existingEnabled === newEnabled ? skipOp : { op: 'enable', option: newOption };
} else {
if (Log.isDebug) {
Log.log(
`Expected the "enabled" attribute in ${id} but it was not present (attributes: ${JSON.stringify(
attributes,
null,
2
)}). Will skip syncing this capability.`
);
}
return skipOp;
}
// If the states don't match, we need to update
const newEnabled = newOption === CapabilityTypeOption.ON;
return existingEnabled === newEnabled ? skipOp : { op: 'enable', option: newOption };
};
export type CapabilityClassifier = {
+122 -78
View File
@@ -21,8 +21,10 @@ export type Scalars = {
Float: { input: number; output: number; }
DateTime: { input: any; output: any; }
DevDomainName: { input: any; output: any; }
EnvironmentVariableEnvironment: { input: any; output: any; }
JSON: { input: any; output: any; }
JSONObject: { input: any; output: any; }
SubmissionAndroidTrack: { input: any; output: any; }
WorkerDeploymentIdentifier: { input: any; output: any; }
WorkerDeploymentRequestID: { input: any; output: any; }
};
@@ -122,6 +124,7 @@ export type Account = {
displayName?: Maybe<Scalars['String']['output']>;
/** Environment secrets for an account */
environmentSecrets: Array<EnvironmentSecret>;
environmentVariableEnvironments: Array<Scalars['EnvironmentVariableEnvironment']['output']>;
/** Environment variables for an account */
environmentVariables: Array<EnvironmentVariable>;
/** Environment variables for an account with decrypted secret values */
@@ -386,7 +389,7 @@ export type AccountEnvironmentSecretsArgs = {
* data and settings. Actors may own and be members of accounts.
*/
export type AccountEnvironmentVariablesArgs = {
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
};
@@ -396,7 +399,7 @@ export type AccountEnvironmentVariablesArgs = {
* data and settings. Actors may own and be members of accounts.
*/
export type AccountEnvironmentVariablesIncludingSensitiveArgs = {
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
};
@@ -1125,7 +1128,7 @@ export type AndroidJobInput = {
cache?: InputMaybe<BuildCacheInput>;
customBuildConfig?: InputMaybe<CustomBuildConfigInput>;
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
experimental?: InputMaybe<Scalars['JSONObject']['input']>;
gradleCommand?: InputMaybe<Scalars['String']['input']>;
loggerLevel?: InputMaybe<WorkerLoggerLevel>;
@@ -1235,12 +1238,11 @@ export type AndroidSubmissionConfig = {
archiveType?: Maybe<SubmissionAndroidArchiveType>;
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
rollout?: Maybe<Scalars['Float']['output']>;
track: SubmissionAndroidTrack;
track: Scalars['String']['output'];
};
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']>;
@@ -1248,7 +1250,7 @@ export type AndroidSubmissionConfigInput = {
isVerboseFastlaneEnabled?: InputMaybe<Scalars['Boolean']['input']>;
releaseStatus?: InputMaybe<SubmissionAndroidReleaseStatus>;
rollout?: InputMaybe<Scalars['Float']['input']>;
track: SubmissionAndroidTrack;
track: Scalars['SubmissionAndroidTrack']['input'];
};
/** Represents an Exponent App (or Experience in legacy terms) */
@@ -1284,6 +1286,7 @@ export type App = Project & {
devDomainName?: Maybe<AppDevDomainName>;
/** Environment secrets for an app */
environmentSecrets: Array<EnvironmentSecret>;
environmentVariableEnvironments: Array<Scalars['EnvironmentVariableEnvironment']['output']>;
/** Environment variables for an app */
environmentVariables: Array<EnvironmentVariable>;
/** Environment variables for an app with decrypted secret values */
@@ -1511,14 +1514,14 @@ export type AppEnvironmentSecretsArgs = {
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppEnvironmentVariablesArgs = {
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppEnvironmentVariablesIncludingSensitiveArgs = {
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
};
@@ -2944,7 +2947,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
reactNativeVersion?: Maybe<Scalars['String']['output']>;
releaseChannel?: Maybe<Scalars['String']['output']>;
requiredPackageManager?: Maybe<Scalars['String']['output']>;
resolvedEnvironment?: Maybe<EnvironmentVariableEnvironment>;
resolvedEnvironment?: Maybe<Scalars['EnvironmentVariableEnvironment']['output']>;
/**
* The builder resource class requested by the developer
* @deprecated Use resourceClassDisplayName instead
@@ -2964,7 +2967,9 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
submissions: Array<Submission>;
updateChannel?: Maybe<UpdateChannel>;
updatedAt: Scalars['DateTime']['output'];
waiverType?: Maybe<EasBuildWaiverType>;
workerStartedAt?: Maybe<Scalars['DateTime']['output']>;
workflowJob?: Maybe<WorkflowJob>;
};
@@ -3564,7 +3569,6 @@ export type CreateAndConfigureRepositoryInput = {
export type CreateAndroidSubmissionInput = {
appId: Scalars['ID']['input'];
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
archiveUrl?: InputMaybe<Scalars['String']['input']>;
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']['input']>;
};
@@ -3582,7 +3586,7 @@ export type CreateEnvironmentSecretInput = {
};
export type CreateEnvironmentVariableInput = {
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
environments?: InputMaybe<Array<Scalars['EnvironmentVariableEnvironment']['input']>>;
fileName?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
overwrite?: InputMaybe<Scalars['Boolean']['input']>;
@@ -3605,7 +3609,7 @@ export type CreateGitHubBuildTriggerInput = {
appId: Scalars['ID']['input'];
autoSubmit: Scalars['Boolean']['input'];
buildProfile: Scalars['String']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
executionBehavior: GitHubBuildTriggerExecutionBehavior;
isActive: Scalars['Boolean']['input'];
platform: AppPlatform;
@@ -3642,7 +3646,6 @@ export type CreateGitHubRepositorySettingsInput = {
export type CreateIosSubmissionInput = {
appId: Scalars['ID']['input'];
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
archiveUrl?: InputMaybe<Scalars['String']['input']>;
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']['input']>;
};
@@ -3654,9 +3657,8 @@ export type CreateSentryProjectInput = {
};
export type CreateSharedEnvironmentVariableInput = {
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
environments?: InputMaybe<Array<Scalars['EnvironmentVariableEnvironment']['input']>>;
fileName?: InputMaybe<Scalars['String']['input']>;
isGlobal?: InputMaybe<Scalars['Boolean']['input']>;
name: Scalars['String']['input'];
overwrite?: InputMaybe<Scalars['Boolean']['input']>;
type?: InputMaybe<EnvironmentSecretType>;
@@ -4231,11 +4233,11 @@ export type EnvironmentVariable = {
/** @deprecated Environment variables are automatically linked to all apps */
apps: Array<App>;
createdAt: Scalars['DateTime']['output'];
environments?: Maybe<Array<EnvironmentVariableEnvironment>>;
environments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
fileName?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
isGlobal?: Maybe<Scalars['Boolean']['output']>;
linkedEnvironments?: Maybe<Array<EnvironmentVariableEnvironment>>;
linkedEnvironments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
name: Scalars['String']['output'];
scope: EnvironmentVariableScope;
type: EnvironmentSecretType;
@@ -4255,12 +4257,6 @@ export type EnvironmentVariableValueArgs = {
includeFileContent?: InputMaybe<Scalars['Boolean']['input']>;
};
export enum EnvironmentVariableEnvironment {
Development = 'DEVELOPMENT',
Preview = 'PREVIEW',
Production = 'PRODUCTION'
}
export type EnvironmentVariableMutation = {
__typename?: 'EnvironmentVariableMutation';
/** Create bulk env variables for an Account */
@@ -4340,11 +4336,11 @@ export type EnvironmentVariableWithSecret = {
__typename?: 'EnvironmentVariableWithSecret';
apps: Array<App>;
createdAt: Scalars['DateTime']['output'];
environments?: Maybe<Array<EnvironmentVariableEnvironment>>;
environments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
fileName?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
isGlobal: Scalars['Boolean']['output'];
linkedEnvironments?: Maybe<Array<EnvironmentVariableEnvironment>>;
linkedEnvironments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
name: Scalars['String']['output'];
scope: EnvironmentVariableScope;
sensitive: Scalars['Boolean']['output'];
@@ -4384,11 +4380,24 @@ export type EstimatedUsage = {
id: Scalars['ID']['output'];
limit: Scalars['Float']['output'];
metricType: UsageMetricType;
platformBreakdown?: Maybe<EstimatedUsagePlatformBreakdown>;
service: EasService;
serviceMetric: EasServiceMetric;
value: Scalars['Float']['output'];
};
export type EstimatedUsagePlatformBreakdown = {
__typename?: 'EstimatedUsagePlatformBreakdown';
android: EstimatedUsagePlatformDetail;
ios: EstimatedUsagePlatformDetail;
};
export type EstimatedUsagePlatformDetail = {
__typename?: 'EstimatedUsagePlatformDetail';
limit: Scalars['Float']['output'];
value: Scalars['Float']['output'];
};
export enum Experiment {
Orbit = 'ORBIT'
}
@@ -4649,7 +4658,7 @@ export type GitHubBuildInput = {
autoSubmit?: InputMaybe<Scalars['Boolean']['input']>;
baseDirectory?: InputMaybe<Scalars['String']['input']>;
buildProfile: Scalars['String']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
gitRef: Scalars['String']['input'];
platform: AppPlatform;
submitProfile?: InputMaybe<Scalars['String']['input']>;
@@ -4661,7 +4670,7 @@ export type GitHubBuildTrigger = {
autoSubmit: Scalars['Boolean']['output'];
buildProfile: Scalars['String']['output'];
createdAt: Scalars['DateTime']['output'];
environment?: Maybe<EnvironmentVariableEnvironment>;
environment?: Maybe<Scalars['EnvironmentVariableEnvironment']['output']>;
executionBehavior: GitHubBuildTriggerExecutionBehavior;
id: Scalars['ID']['output'];
isActive: Scalars['Boolean']['output'];
@@ -5240,7 +5249,7 @@ export type IosJobInput = {
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
experimental?: InputMaybe<Scalars['JSONObject']['input']>;
loggerLevel?: InputMaybe<WorkerLoggerLevel>;
mode?: InputMaybe<BuildMode>;
@@ -5322,7 +5331,6 @@ export type IosSubmissionConfig = {
export type IosSubmissionConfigInput = {
appleAppSpecificPassword?: InputMaybe<Scalars['String']['input']>;
appleIdUsername?: InputMaybe<Scalars['String']['input']>;
archiveUrl?: InputMaybe<Scalars['String']['input']>;
ascApiKey?: InputMaybe<AscApiKeyInput>;
ascApiKeyId?: InputMaybe<Scalars['String']['input']>;
ascAppIdentifier: Scalars['String']['input'];
@@ -5967,7 +5975,7 @@ export type PublishUpdateGroupInput = {
assetMapGroup?: InputMaybe<AssetMapGroup>;
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']['input']>;
branchId: Scalars['String']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
excludedAssets?: InputMaybe<Array<PartialManifestAsset>>;
fingerprintInfoGroup?: InputMaybe<FingerprintInfoGroup>;
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
@@ -6931,6 +6939,7 @@ export type Submission = ActivityTimelineProjectActivity & {
status: SubmissionStatus;
submittedBuild?: Maybe<Build>;
updatedAt: Scalars['DateTime']['output'];
workflowJob?: Maybe<WorkflowJob>;
};
export enum SubmissionAndroidArchiveType {
@@ -6945,13 +6954,6 @@ export enum SubmissionAndroidReleaseStatus {
InProgress = 'IN_PROGRESS'
}
export enum SubmissionAndroidTrack {
Alpha = 'ALPHA',
Beta = 'BETA',
Internal = 'INTERNAL',
Production = 'PRODUCTION'
}
export type SubmissionArchiveSourceInput = {
/** Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE, GCS_BUILD_APPLICATION_ARCHIVE_ORCHESTRATOR or GCS_SUBMIT_ARCHIVE */
bucketKey?: InputMaybe<Scalars['String']['input']>;
@@ -7122,7 +7124,7 @@ export type Update = ActivityTimelineProjectActivity & {
codeSigningInfo?: Maybe<CodeSigningInfo>;
createdAt: Scalars['DateTime']['output'];
deployments: DeploymentResult;
environment?: Maybe<EnvironmentVariableEnvironment>;
environment?: Maybe<Scalars['EnvironmentVariableEnvironment']['output']>;
expoGoSDKVersion?: Maybe<Scalars['String']['output']>;
fingerprint?: Maybe<Fingerprint>;
gitCommitHash?: Maybe<Scalars['String']['output']>;
@@ -7144,6 +7146,7 @@ export type Update = ActivityTimelineProjectActivity & {
/** @deprecated Use 'runtime' field . */
runtimeVersion: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
workflowJob?: Maybe<WorkflowJob>;
};
@@ -7354,10 +7357,9 @@ export type UpdateDeploymentsConnection = {
};
export type UpdateEnvironmentVariableInput = {
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
environments?: InputMaybe<Array<Scalars['EnvironmentVariableEnvironment']['input']>>;
fileName?: InputMaybe<Scalars['String']['input']>;
id: Scalars['ID']['input'];
isGlobal?: InputMaybe<Scalars['Boolean']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
type?: InputMaybe<EnvironmentSecretType>;
value?: InputMaybe<Scalars['String']['input']>;
@@ -7373,7 +7375,7 @@ export type UpdateFilterInput = {
export type UpdateGitHubBuildTriggerInput = {
autoSubmit: Scalars['Boolean']['input'];
buildProfile: Scalars['String']['input'];
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
executionBehavior: GitHubBuildTriggerExecutionBehavior;
isActive: Scalars['Boolean']['input'];
platform: AppPlatform;
@@ -8882,6 +8884,7 @@ export enum WorkflowJobType {
Doc = 'DOC',
Fingerprint = 'FINGERPRINT',
GetBuild = 'GET_BUILD',
GithubComment = 'GITHUB_COMMENT',
MaestroCloud = 'MAESTRO_CLOUD',
MaestroTest = 'MAESTRO_TEST',
Repack = 'REPACK',
@@ -8946,10 +8949,18 @@ export type WorkflowRevisionInput = {
export type WorkflowRevisionMutation = {
__typename?: 'WorkflowRevisionMutation';
getOrCreateWorkflowRevisionFromGitRef: WorkflowRevision;
validateWorkflowYamlConfig: Scalars['Boolean']['output'];
};
export type WorkflowRevisionMutationGetOrCreateWorkflowRevisionFromGitRefArgs = {
appId: Scalars['ID']['input'];
fileName: Scalars['String']['input'];
gitRef: Scalars['String']['input'];
};
export type WorkflowRevisionMutationValidateWorkflowYamlConfigArgs = {
appId: Scalars['ID']['input'];
yamlConfig: Scalars['String']['input'];
@@ -9028,6 +9039,7 @@ export type WorkflowRunMutation = {
__typename?: 'WorkflowRunMutation';
cancelWorkflowRun: WorkflowRun;
createWorkflowRun: WorkflowRun;
createWorkflowRunFromGitRef: WorkflowRun;
retryWorkflowRun: WorkflowRun;
};
@@ -9044,6 +9056,13 @@ export type WorkflowRunMutationCreateWorkflowRunArgs = {
};
export type WorkflowRunMutationCreateWorkflowRunFromGitRefArgs = {
gitRef: Scalars['String']['input'];
inputs?: InputMaybe<Scalars['JSONObject']['input']>;
workflowRevisionId: Scalars['ID']['input'];
};
export type WorkflowRunMutationRetryWorkflowRunArgs = {
fromFailedJobs?: InputMaybe<Scalars['Boolean']['input']>;
workflowRunId: Scalars['ID']['input'];
@@ -9691,7 +9710,7 @@ export type CreateEnvironmentVariableForAccountMutationVariables = Exact<{
}>;
export type CreateEnvironmentVariableForAccountMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForAccount: { __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 CreateEnvironmentVariableForAccountMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForAccount: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
export type CreateEnvironmentVariableForAppMutationVariables = Exact<{
input: CreateEnvironmentVariableInput;
@@ -9699,14 +9718,14 @@ export type CreateEnvironmentVariableForAppMutationVariables = Exact<{
}>;
export type CreateEnvironmentVariableForAppMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForApp: { __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 CreateEnvironmentVariableForAppMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForApp: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
export type UpdateEnvironmentVariableMutationVariables = Exact<{
input: UpdateEnvironmentVariableInput;
}>;
export type UpdateEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', updateEnvironmentVariable: { __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 UpdateEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', updateEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
export type DeleteEnvironmentVariableMutationVariables = Exact<{
id: Scalars['ID']['input'];
@@ -9758,7 +9777,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, 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 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?: any | 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'];
@@ -9774,7 +9793,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, 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 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?: any | 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'];
@@ -9784,7 +9803,7 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
}>;
export type CreateAndroidSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createAndroidSubmission: { __typename?: 'CreateSubmissionResult', submission: { __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 CreateAndroidSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createAndroidSubmission: { __typename?: 'CreateSubmissionResult', submission: { __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: string, 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 CreateIosSubmissionMutationVariables = Exact<{
appId: Scalars['ID']['input'];
@@ -9794,7 +9813,7 @@ export type CreateIosSubmissionMutationVariables = Exact<{
}>;
export type CreateIosSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createIosSubmission: { __typename?: 'CreateSubmissionResult', submission: { __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 CreateIosSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createIosSubmission: { __typename?: 'CreateSubmissionResult', submission: { __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: string, 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 CreateUploadSessionMutationVariables = Exact<{
type: UploadSessionType;
@@ -9842,6 +9861,15 @@ export type DeleteWebhookMutationVariables = Exact<{
export type DeleteWebhookMutation = { __typename?: 'RootMutation', webhook: { __typename?: 'WebhookMutation', deleteWebhook: { __typename?: 'DeleteWebhookResult', id: string } } };
export type GetOrCreateWorkflowRevisionFromGitRefMutationVariables = Exact<{
appId: Scalars['ID']['input'];
fileName: Scalars['String']['input'];
gitRef: Scalars['String']['input'];
}>;
export type GetOrCreateWorkflowRevisionFromGitRefMutation = { __typename?: 'RootMutation', workflowRevision: { __typename?: 'WorkflowRevisionMutation', getOrCreateWorkflowRevisionFromGitRef: { __typename?: 'WorkflowRevision', id: string, yamlConfig: string, blobSha: string, commitSha?: string | null, createdAt: any, workflow: { __typename?: 'Workflow', id: string } } } };
export type ValidateWorkflowYamlConfigMutationVariables = Exact<{
appId: Scalars['ID']['input'];
yamlConfig: Scalars['String']['input'];
@@ -9859,6 +9887,15 @@ export type CreateWorkflowRunMutationVariables = Exact<{
export type CreateWorkflowRunMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', createWorkflowRun: { __typename?: 'WorkflowRun', id: string } } };
export type CreateWorkflowRunFromGitRefMutationVariables = Exact<{
workflowRevisionId: Scalars['ID']['input'];
gitRef: Scalars['String']['input'];
inputs?: InputMaybe<Scalars['JSONObject']['input']>;
}>;
export type CreateWorkflowRunFromGitRefMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', createWorkflowRunFromGitRef: { __typename?: 'WorkflowRun', id: string } } };
export type CancelWorkflowRunMutationVariables = Exact<{
workflowRunId: Scalars['ID']['input'];
}>;
@@ -9885,7 +9922,7 @@ export type AppByIdWorkflowsQueryVariables = Exact<{
}>;
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 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, revisionsPaginated: { __typename?: 'WorkflowRevisionsConnection', edges: Array<{ __typename?: 'WorkflowRevisionEdge', node: { __typename?: 'WorkflowRevision', id: string, blobSha: string, commitSha?: string | null, createdAt: any, yamlConfig: string } }> } }> } } };
export type AppByIdWorkflowRunsFilteredByStatusQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -9944,7 +9981,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, 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 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?: any | 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'];
@@ -9965,7 +10002,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, 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 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?: any | 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'];
@@ -9979,7 +10016,7 @@ export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
export type BuildsWithFingerprintByIdQueryVariables = Exact<{
buildId: Scalars['ID']['input'];
@@ -10013,7 +10050,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, 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 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?: any | 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'];
@@ -10022,7 +10059,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, 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 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?: any | 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'];
@@ -10042,45 +10079,52 @@ export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
export type EnvironmentSecretsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> }, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> } } };
export type AppEnvironmentVariableEnvironmentsQueryVariables = Exact<{
appId: Scalars['String']['input'];
}>;
export type AppEnvironmentVariableEnvironmentsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariableEnvironments: Array<any> } } };
export type EnvironmentVariablesIncludingSensitiveByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
includeFileContent: Scalars['Boolean']['input'];
}>;
export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
export type EnvironmentVariablesByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
includeFileContent: Scalars['Boolean']['input'];
}>;
export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<EnvironmentVariableEnvironment> | null, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<any> | null, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
export type EnvironmentVariablesSharedQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
includeFileContent: Scalars['Boolean']['input'];
}>;
export type EnvironmentVariablesSharedQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<EnvironmentVariableEnvironment> | null, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
export type EnvironmentVariablesSharedQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<any> | null, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
export type EnvironmentVariablesSharedWithSensitiveQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
includeFileContent: Scalars['Boolean']['input'];
}>;
export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
export type FingerprintsByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -10140,7 +10184,7 @@ export type SubmissionsByIdQueryVariables = Exact<{
}>;
export type SubmissionsByIdQuery = { __typename?: 'RootQuery', submissions: { __typename?: 'SubmissionQuery', byId: { __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 SubmissionsByIdQuery = { __typename?: 'RootQuery', submissions: { __typename?: 'SubmissionQuery', byId: { __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: string, 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 GetAllSubmissionsForAppQueryVariables = Exact<{
appId: Scalars['String']['input'];
@@ -10151,14 +10195,14 @@ export type GetAllSubmissionsForAppQueryVariables = Exact<{
}>;
export type GetAllSubmissionsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }> } } };
export type GetAllSubmissionsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, 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 ViewUpdatesByGroupQueryVariables = Exact<{
groupId: Scalars['ID']['input'];
}>;
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 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?: any | 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'];
@@ -10169,7 +10213,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, 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 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?: any | 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'];
@@ -10179,14 +10223,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, 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 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?: any | 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, 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 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?: any | 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; }>;
@@ -10213,7 +10257,7 @@ export type WorkflowJobByIdQueryVariables = Exact<{
}>;
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
export type WorkflowRunByIdQueryVariables = Exact<{
workflowRunId: Scalars['ID']['input'];
@@ -10227,7 +10271,7 @@ export type WorkflowRunByIdWithJobsQueryVariables = Exact<{
}>;
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
export type WorkflowRunsForAppIdFileNameAndStatusQueryVariables = Exact<{
appId: Scalars['ID']['input'];
@@ -10247,15 +10291,15 @@ export type BackgroundJobReceiptDataFragment = { __typename?: 'BackgroundJobRece
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType };
export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType };
export type EnvironmentVariableWithSecretFragment = { __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType };
export type EnvironmentVariableWithSecretFragment = { __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType };
export type FingerprintFragment = { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } };
@@ -10263,11 +10307,11 @@ export type RuntimeFragment = { __typename?: 'Runtime', id: string, version: str
export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id: string, name: StatuspageServiceName, status: StatuspageServiceStatus, incidents: Array<{ __typename?: 'StatuspageIncident', id: string, status: StatuspageIncidentStatus, name: string, impact: StatuspageIncidentImpact, shortlink: string }> };
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
export type 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: string, 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, 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 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?: any | 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, 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, isGitWorkingTreeDirty: boolean, environment?: any | 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 };
@@ -10275,9 +10319,9 @@ 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 WorkflowFragment = { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any, revisionsPaginated: { __typename?: 'WorkflowRevisionsConnection', edges: Array<{ __typename?: 'WorkflowRevisionEdge', node: { __typename?: 'WorkflowRevision', id: string, blobSha: string, commitSha?: string | null, createdAt: any, yamlConfig: string } }> } };
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
export type WorkflowRunFragment = { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }>, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string } };
@@ -7,37 +7,19 @@ import {
CreateBulkEnvironmentVariablesForAppMutation,
CreateEnvironmentVariableForAccountMutation,
CreateEnvironmentVariableForAppMutation,
CreateEnvironmentVariableInput,
CreateSharedEnvironmentVariableInput,
DeleteEnvironmentVariableMutation,
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableVisibility,
UpdateEnvironmentVariableInput,
UpdateEnvironmentVariableMutation,
} from '../generated';
import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
type CreateVariableArgs = {
value: string;
name: string;
visibility: EnvironmentVariableVisibility;
environments: EnvironmentVariableEnvironment[];
type: EnvironmentSecretType;
isGlobal?: boolean;
fileName?: string;
};
export type EnvironmentVariablePushInput = {
name: string;
value: string;
environments: EnvironmentVariableEnvironment[];
visibility: EnvironmentVariableVisibility;
overwrite?: boolean;
};
export const EnvironmentVariableMutation = {
async createSharedVariableAsync(
graphqlClient: ExpoGraphqlClient,
input: CreateVariableArgs,
input: CreateSharedEnvironmentVariableInput,
accountId: string
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
@@ -69,7 +51,7 @@ export const EnvironmentVariableMutation = {
},
async createForAppAsync(
graphqlClient: ExpoGraphqlClient,
input: CreateVariableArgs,
input: CreateEnvironmentVariableInput,
appId: string
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
@@ -98,7 +80,7 @@ export const EnvironmentVariableMutation = {
},
async updateAsync(
graphqlClient: ExpoGraphqlClient,
input: Partial<CreateVariableArgs> & { id: string }
input: UpdateEnvironmentVariableInput
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -143,7 +125,7 @@ export const EnvironmentVariableMutation = {
},
async createBulkEnvironmentVariablesForAppAsync(
graphqlClient: ExpoGraphqlClient,
input: EnvironmentVariablePushInput[],
input: CreateEnvironmentVariableInput[],
appId: string
): Promise<boolean> {
await withErrorHandlingAsync(
@@ -1,17 +1,20 @@
import { FingerprintSource } from '@expo/eas-build-job';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import { CreateFingeprintMutation, FingerprintFragment } from '../generated';
import {
CreateFingeprintMutation,
FingerprintFragment,
FingerprintSourceInput,
} from '../generated';
import { FingerprintFragmentNode } from '../types/Fingerprint';
export const FingerprintMutation = {
async createFingerprintAsync(
graphqlClient: ExpoGraphqlClient,
appId: string,
fingerprintData: { hash: string; source?: FingerprintSource }
fingerprintData: { hash: string; source?: FingerprintSourceInput }
): Promise<FingerprintFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -4,8 +4,11 @@ import { z } from 'zod';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
GetOrCreateWorkflowRevisionFromGitRefMutation,
GetOrCreateWorkflowRevisionFromGitRefMutationVariables,
ValidateWorkflowYamlConfigMutation,
ValidateWorkflowYamlConfigMutationVariables,
WorkflowRevision,
} from '../generated';
export namespace WorkflowRevisionMutation {
@@ -17,6 +20,61 @@ export namespace WorkflowRevisionMutation {
}),
});
export async function getOrCreateWorkflowRevisionFromGitRefAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
fileName,
gitRef,
}: {
appId: string;
fileName: string;
gitRef: string;
}
): Promise<WorkflowRevision | undefined> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
GetOrCreateWorkflowRevisionFromGitRefMutation,
GetOrCreateWorkflowRevisionFromGitRefMutationVariables
>(
gql`
mutation GetOrCreateWorkflowRevisionFromGitRef(
$appId: ID!
$fileName: String!
$gitRef: String!
) {
workflowRevision {
getOrCreateWorkflowRevisionFromGitRef(
appId: $appId
fileName: $fileName
gitRef: $gitRef
) {
id
yamlConfig
blobSha
commitSha
createdAt
workflow {
id
}
}
}
}
`,
{
appId,
fileName,
gitRef,
}
)
.toPromise()
);
return (
(data.workflowRevision?.getOrCreateWorkflowRevisionFromGitRef as WorkflowRevision) ??
undefined
);
}
export async function validateWorkflowYamlConfigAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -5,6 +5,8 @@ import { withErrorHandlingAsync } from '../client';
import {
CancelWorkflowRunMutation,
CancelWorkflowRunMutationVariables,
CreateWorkflowRunFromGitRefMutation,
CreateWorkflowRunFromGitRefMutationVariables,
CreateWorkflowRunMutation,
CreateWorkflowRunMutationVariables,
WorkflowRevisionInput,
@@ -54,6 +56,53 @@ export namespace WorkflowRunMutation {
);
return { id: data.workflowRun.createWorkflowRun.id };
}
export async function createWorkflowRunFromGitRefAsync(
graphqlClient: ExpoGraphqlClient,
{
workflowRevisionId,
gitRef,
inputs,
}: {
workflowRevisionId: string;
gitRef: string;
inputs?: Record<string, any>;
}
): Promise<{ id: string }> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
CreateWorkflowRunFromGitRefMutation,
CreateWorkflowRunFromGitRefMutationVariables
>(
gql`
mutation CreateWorkflowRunFromGitRef(
$workflowRevisionId: ID!
$gitRef: String!
$inputs: JSONObject
) {
workflowRun {
createWorkflowRunFromGitRef(
workflowRevisionId: $workflowRevisionId
gitRef: $gitRef
inputs: $inputs
) {
id
}
}
}
`,
{
workflowRevisionId,
gitRef,
inputs,
}
)
.toPromise()
);
return { id: data.workflowRun.createWorkflowRunFromGitRef.id };
}
export async function cancelWorkflowRunAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -4,7 +4,6 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariablesByAppIdQuery,
EnvironmentVariablesSharedQuery,
@@ -14,7 +13,7 @@ import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
import { EnvironmentVariableWithSecretFragmentNode } from '../types/EnvironmentVariableWithSecret';
type EnvironmentVariableWithLinkedEnvironments = EnvironmentVariableFragment & {
linkedEnvironments?: EnvironmentVariableEnvironment[] | null;
linkedEnvironments?: string[] | null;
};
export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
@@ -22,6 +21,31 @@ export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
};
export const EnvironmentVariablesQuery = {
async environmentVariableEnvironmentsAsync(
graphqlClient: ExpoGraphqlClient,
appId: string
): Promise<string[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
gql`
query AppEnvironmentVariableEnvironments($appId: String!) {
app {
byId(appId: $appId) {
id
environmentVariableEnvironments
}
}
}
`,
{ appId },
{ additionalTypenames: ['App'] }
)
.toPromise()
);
return data.app?.byId.environmentVariableEnvironments ?? [];
},
async byAppIdWithSensitiveAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -31,7 +55,7 @@ export const EnvironmentVariablesQuery = {
includeFileContent = false,
}: {
appId: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
filterNames?: string[];
includeFileContent?: boolean;
}
@@ -80,7 +104,7 @@ export const EnvironmentVariablesQuery = {
includeFileContent = false,
}: {
appId: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
filterNames?: string[];
includeFileContent?: boolean;
}
@@ -128,7 +152,7 @@ export const EnvironmentVariablesQuery = {
}: {
appId: string;
filterNames?: string[];
environment?: EnvironmentVariableEnvironment;
environment?: string;
includeFileContent?: boolean;
}
): Promise<(EnvironmentVariableWithFileContent & EnvironmentVariableWithLinkedEnvironments)[]> {
@@ -178,7 +202,7 @@ export const EnvironmentVariablesQuery = {
}: {
appId: string;
filterNames?: string[];
environment?: EnvironmentVariableEnvironment;
environment?: string;
includeFileContent?: boolean;
}
): Promise<EnvironmentVariableWithFileContent[]> {
@@ -7,5 +7,16 @@ export const WorkflowFragmentNode = gql`
fileName
createdAt
updatedAt
revisionsPaginated(first: 1) {
edges {
node {
id
blobSha
commitSha
createdAt
yamlConfig
}
}
}
}
`;
@@ -1,5 +1,7 @@
import gql from 'graphql-tag';
import { BuildFragmentNode } from './Build';
export const WorkflowJobFragmentNode = gql`
fragment WorkflowJobFragment on WorkflowJob {
id
@@ -13,11 +15,23 @@ export const WorkflowJobFragmentNode = gql`
turtleJobRun {
id
logFileUrls
artifacts {
id
name
contentType
fileSizeBytes
filename
downloadUrl
}
errors {
errorCode
message
}
}
turtleBuild {
id
...BuildFragment
}
outputs
errors {
title
@@ -26,4 +40,5 @@ export const WorkflowJobFragmentNode = gql`
createdAt
updatedAt
}
${BuildFragmentNode}
`;
+2 -1
View File
@@ -38,7 +38,8 @@ export default class Log {
if (Log.isDebug) {
Log.consoleLog(...args);
} else {
nodeDebug(args);
Log.updateIsLastLineNewLine(args);
nodeDebug(...args);
}
}
@@ -0,0 +1,41 @@
import { selectAsync } from '../../prompts';
import { PackageManager, promptForPackageManagerAsync } from '../installDependencies';
jest.mock('../../prompts');
describe('installDependencies', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('promptForPackageManagerAsync', () => {
it.each(['npm', 'yarn', 'pnpm'])('returns %s when user selects %s', async packageManager => {
jest.mocked(selectAsync).mockResolvedValue(packageManager as PackageManager);
const result = await promptForPackageManagerAsync();
expect(selectAsync).toHaveBeenCalledWith(
'Which package manager would you like to use?',
[
{ title: 'npm', value: 'npm' },
{ title: 'Yarn', value: 'yarn' },
{ title: 'pnpm', value: 'pnpm' },
],
{ initial: 'npm' }
);
expect(result).toBe(packageManager);
});
it('defaults to npm as initial selection', async () => {
jest.mocked(selectAsync).mockResolvedValue('npm');
await promptForPackageManagerAsync();
expect(selectAsync).toHaveBeenCalledWith(
expect.any(String),
expect.any(Array),
expect.objectContaining({ initial: 'npm' })
);
});
});
});
@@ -1,13 +1,29 @@
import { runCommandAsync } from './runCommand';
import { selectAsync } from '../prompts';
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
export async function promptForPackageManagerAsync(): Promise<PackageManager> {
return await selectAsync(
'Which package manager would you like to use?',
[
{ title: 'npm', value: 'npm' },
{ title: 'Yarn', value: 'yarn' },
{ title: 'pnpm', value: 'pnpm' },
],
{ initial: 'npm' }
);
}
export async function installDependenciesAsync({
projectDir,
packageManager = 'npm',
}: {
projectDir: string;
packageManager?: PackageManager;
}): Promise<void> {
// TODO: add support for other package managers
await runCommandAsync({
command: 'npm',
command: packageManager,
args: ['install'],
cwd: projectDir,
shouldShowStderrLine: line => {
@@ -16,7 +32,7 @@ export async function installDependenciesAsync({
!line.includes('deprecated') &&
!line.includes('no longer maintained') &&
!line.includes('has been moved') &&
!(line === 'npm')
!(line === packageManager)
);
},
});
@@ -1,11 +1,14 @@
import { FingerprintSource, FingerprintSourceType } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { LocalBuildMode } from '../build/local';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { UploadSessionType } from '../graphql/generated';
import {
FingerprintSourceInput,
FingerprintSourceType,
UploadSessionType,
} from '../graphql/generated';
import Log from '../log';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { getTmpDirectory } from '../utils/paths';
@@ -25,8 +28,15 @@ export async function maybeUploadFingerprintAsync({
localBuildMode?: LocalBuildMode;
}): Promise<{
hash: string;
fingerprintSource?: FingerprintSource;
fingerprintSource?: FingerprintSourceInput;
}> {
if (localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
// We're not uploading local build fingerprints to EAS
return {
hash,
};
}
await fs.mkdirp(getTmpDirectory());
const fingerprintLocation = path.join(getTmpDirectory(), `${uuidv4()}-runtime-fingerprint.json`);
@@ -35,17 +45,6 @@ export async function maybeUploadFingerprintAsync({
sources: fingerprint.fingerprintSources,
});
if (localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
hash,
fingerprintSource: {
type: FingerprintSourceType.PATH,
path: fingerprintLocation,
isDebugFingerprint: fingerprint.isDebugFingerprintSource,
},
};
}
let fingerprintGCSBucketKey = undefined;
try {
fingerprintGCSBucketKey = await uploadFileAtPathToGCSAsync(
@@ -71,7 +70,7 @@ export async function maybeUploadFingerprintAsync({
return {
hash,
fingerprintSource: {
type: FingerprintSourceType.GCS,
type: FingerprintSourceType.Gcs,
bucketKey: fingerprintGCSBucketKey,
isDebugFingerprint: fingerprint.isDebugFingerprintSource,
},
+7 -6
View File
@@ -1,6 +1,6 @@
import { ExpoConfig, Platform as ExpoConfigPlatform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Env, FingerprintSource, Platform, Workflow } from '@expo/eas-build-job';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import chalk from 'chalk';
@@ -25,6 +25,7 @@ import {
AppPlatform,
AssetMetadataStatus,
BuildFragment,
FingerprintSourceInput,
PartialManifestAsset,
UpdateRolloutInfo,
UpdateRolloutInfoGroup,
@@ -749,7 +750,7 @@ type FingerprintInfoGroup = {
type FingerprintInfo = {
fingerprintHash: string;
fingerprintSource: FingerprintSource;
fingerprintSource: FingerprintSourceInput;
};
export async function getRuntimeVersionInfoObjectsAsync({
@@ -904,14 +905,14 @@ export async function maybeCalculateFingerprintForRuntimeVersionInfoObjectsWitho
graphqlClient: ExpoGraphqlClient;
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
expoUpdatesRuntimeFingerprintSource: FingerprintSource | null;
expoUpdatesRuntimeFingerprintSource: FingerprintSourceInput | null;
})[];
workflowsByPlatform: Record<Platform, Workflow>;
env: Env | undefined;
}): Promise<
(RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
expoUpdatesRuntimeFingerprintSource: FingerprintSource | null;
expoUpdatesRuntimeFingerprintSource: FingerprintSourceInput | null;
fingerprintInfoGroup: FingerprintInfoGroup;
})[]
> {
@@ -959,7 +960,7 @@ export async function maybeCalculateFingerprintForRuntimeVersionInfoObjectsWitho
for (const platform of runtimeInfo.platforms) {
const runtimeAndPlatform = `${runtimeInfo.runtimeVersion}-${platform}`;
const fingerprint = uploadedFingerprintsByRuntimeAndPlatform.get(runtimeAndPlatform);
if (fingerprint && fingerprint.uploadedSource) {
if (fingerprint?.uploadedSource) {
fingerprintInfoGroup[platform] = {
fingerprintHash: fingerprint.hash,
fingerprintSource: fingerprint.uploadedSource,
@@ -980,7 +981,7 @@ export async function maybeCalculateFingerprintForRuntimeVersionInfoObjectsWitho
infoGroup
): infoGroup is RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
expoUpdatesRuntimeFingerprintSource: FingerprintSource;
expoUpdatesRuntimeFingerprintSource: FingerprintSourceInput;
expoUpdatesRuntimeFingerprintHash: string;
} =>
!!infoGroup.expoUpdatesRuntimeFingerprintHash &&
@@ -1,10 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import {
AndroidReleaseStatus,
AndroidReleaseTrack,
EasJsonAccessor,
EasJsonUtils,
} from '@expo/eas-json';
import { AndroidReleaseStatus, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { MissingProfileError } from '@expo/eas-json/build/errors';
import { refreshContextSubmitProfileAsync } from '../commons';
@@ -64,7 +59,7 @@ describe(refreshContextSubmitProfileAsync, () => {
it('returns updated context if profile exists', async () => {
const submitContext = {
profile: {
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
applicationId: 'appId1',
@@ -79,7 +74,7 @@ describe(refreshContextSubmitProfileAsync, () => {
_profileName: string | undefined
) => {
return {
track: AndroidReleaseTrack.beta,
track: 'beta',
releaseStatus: AndroidReleaseStatus.inProgress,
changesNotSentForReview: true,
applicationId: 'appId2',
@@ -91,7 +86,7 @@ describe(refreshContextSubmitProfileAsync, () => {
expect(result).toEqual({
profile: {
track: AndroidReleaseTrack.beta,
track: 'beta',
releaseStatus: AndroidReleaseStatus.inProgress,
changesNotSentForReview: true,
applicationId: 'appId2',
@@ -101,7 +96,7 @@ describe(refreshContextSubmitProfileAsync, () => {
it('returns unmodified context if profile does not exist', async () => {
const submitContext = {
profile: {
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
applicationId: 'appId1',
@@ -123,7 +118,7 @@ describe(refreshContextSubmitProfileAsync, () => {
expect(result).toEqual({
profile: {
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
applicationId: 'appId1',
@@ -1,10 +1,10 @@
import { Platform } from '@expo/eas-build-job';
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
import { AndroidReleaseStatus } from '@expo/eas-json';
import { Result, result } from '@expo/results';
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
import { SubmissionAndroidReleaseStatus, SubmissionAndroidTrack } from '../../graphql/generated';
import { SubmissionAndroidReleaseStatus } from '../../graphql/generated';
import Log from '../../log';
import {
AmbiguousApplicationIdError,
@@ -50,12 +50,12 @@ export default class AndroidSubmitCommand {
private async getAndroidSubmissionOptionsAsync(
archiveSource: ArchiveSource
): Promise<AndroidSubmissionOptions> {
const track = this.resolveTrack();
const track = this.ctx.profile.track || 'internal';
const releaseStatus = this.resolveReleaseStatus();
const rollout = this.resolveRollout();
const serviceAccountSource = await this.resolveServiceAccountSourceAsync();
const errored = [track, releaseStatus, serviceAccountSource, rollout].filter(r => !r.ok);
const errored = [releaseStatus, serviceAccountSource, rollout].filter(r => !r.ok);
if (errored.length > 0) {
const message = errored.map(err => err.reason?.message).join('\n');
Log.error(message);
@@ -64,7 +64,7 @@ export default class AndroidSubmitCommand {
return {
projectId: this.ctx.projectId,
track: track.enforceValue(),
track,
releaseStatus: releaseStatus.enforceValue(),
rollout: rollout.enforceValue(),
archiveSource,
@@ -91,27 +91,6 @@ export default class AndroidSubmitCommand {
}
}
private resolveTrack(): Result<SubmissionAndroidTrack> {
const { track } = this.ctx.profile;
if (!track) {
return result(SubmissionAndroidTrack.Internal);
}
const capitalizedTrack = capitalizeFirstLetter(track);
if (capitalizedTrack in SubmissionAndroidTrack) {
return result(
SubmissionAndroidTrack[capitalizedTrack as keyof typeof SubmissionAndroidTrack]
);
} else {
return result(
new Error(
`Unsupported track: ${track} (valid options: ${Object.keys(AndroidReleaseTrack).join(
', '
)})`
)
);
}
}
private resolveReleaseStatus(): Result<SubmissionAndroidReleaseStatus> {
const { releaseStatus } = this.ctx.profile;
if (!releaseStatus) {
@@ -10,7 +10,6 @@ import { SubmissionEvent } from '../../analytics/AnalyticsManager';
import {
AndroidSubmissionConfigInput,
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
SubmissionFragment,
} from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
@@ -142,7 +141,7 @@ type SummaryData = {
projectId: string;
releaseStatus?: SubmissionAndroidReleaseStatus;
rollout?: number;
track: SubmissionAndroidTrack;
track: string;
} & ArchiveSourceSummaryFields;
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
import { AndroidReleaseStatus } from '@expo/eas-json';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
@@ -14,7 +14,6 @@ import {
AppPlatform,
BuildFragment,
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
SubmissionArchiveSourceType,
} from '../../../graphql/generated';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
@@ -118,7 +117,7 @@ describe(AndroidSubmitCommand, () => {
url: 'http://expo.dev/fake.apk',
},
profile: {
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -158,7 +157,7 @@ describe(AndroidSubmitCommand, () => {
},
profile: {
serviceAccountKeyPath: '/google-service-account.json',
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -183,7 +182,7 @@ describe(AndroidSubmitCommand, () => {
config: {
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Internal,
track: 'internal',
changesNotSentForReview: false,
isVerboseFastlaneEnabled: false,
},
@@ -206,7 +205,7 @@ describe(AndroidSubmitCommand, () => {
},
profile: {
serviceAccountKeyPath: '/google-service-account.json',
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.inProgress,
changesNotSentForReview: false,
},
@@ -231,7 +230,7 @@ describe(AndroidSubmitCommand, () => {
config: {
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.InProgress,
track: SubmissionAndroidTrack.Internal,
track: 'internal',
changesNotSentForReview: false,
isVerboseFastlaneEnabled: false,
},
@@ -257,7 +256,7 @@ describe(AndroidSubmitCommand, () => {
},
profile: {
serviceAccountKeyPath: '/google-service-account.json',
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -280,7 +279,7 @@ describe(AndroidSubmitCommand, () => {
config: {
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Internal,
track: 'internal',
changesNotSentForReview: false,
isVerboseFastlaneEnabled: false,
},
@@ -309,7 +308,7 @@ describe(AndroidSubmitCommand, () => {
.mockImplementation(async (ctx: SubmissionContext<Platform>, _archiveProfile: string) => {
ctx.profile = {
serviceAccountKeyPath: '/google-other-service-account.json',
track: AndroidReleaseTrack.beta,
track: 'beta',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
applicationId: 'otherAppId',
@@ -323,7 +322,7 @@ describe(AndroidSubmitCommand, () => {
archiveFlags: {},
profile: {
serviceAccountKeyPath: '/google-service-account.json',
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -350,7 +349,7 @@ describe(AndroidSubmitCommand, () => {
config: {
googleServiceAccountKeyJson: fakeFiles['/google-other-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Beta,
track: 'beta',
changesNotSentForReview: false,
rollout: undefined,
isVerboseFastlaneEnabled: false,
@@ -387,7 +386,7 @@ describe(AndroidSubmitCommand, () => {
archiveFlags: {},
profile: {
serviceAccountKeyPath: '/google-service-account.json',
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -414,7 +413,7 @@ describe(AndroidSubmitCommand, () => {
config: {
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Internal,
track: 'internal',
changesNotSentForReview: false,
rollout: undefined,
isVerboseFastlaneEnabled: false,
@@ -444,7 +443,7 @@ describe(AndroidSubmitCommand, () => {
.mockImplementation(async (ctx: SubmissionContext<Platform>, _archiveProfile: string) => {
ctx.profile = {
serviceAccountKeyPath: '/google-other-service-account.json',
track: AndroidReleaseTrack.beta,
track: 'beta',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
applicationId: 'otherAppId',
@@ -458,7 +457,7 @@ describe(AndroidSubmitCommand, () => {
archiveFlags: {},
profile: {
serviceAccountKeyPath: '/google-service-account.json',
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -486,7 +485,7 @@ describe(AndroidSubmitCommand, () => {
config: {
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
track: SubmissionAndroidTrack.Internal,
track: 'internal',
changesNotSentForReview: false,
rollout: undefined,
isVerboseFastlaneEnabled: false,
@@ -1,5 +1,5 @@
import { Platform } from '@expo/eas-build-job';
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
import { AndroidReleaseStatus } from '@expo/eas-json';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
@@ -151,7 +151,7 @@ describe(getServiceAccountKeyResultAsync, () => {
url: 'http://expo.dev/fake.apk',
},
profile: {
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -195,7 +195,7 @@ describe(getServiceAccountKeyResultAsync, () => {
url: 'http://expo.dev/fake.apk',
},
profile: {
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
@@ -235,7 +235,7 @@ describe(getServiceAccountKeyResultAsync, () => {
url: 'http://expo.dev/fake.apk',
},
profile: {
track: AndroidReleaseTrack.internal,
track: 'internal',
releaseStatus: AndroidReleaseStatus.draft,
changesNotSentForReview: false,
},
+133 -25
View File
@@ -1,12 +1,31 @@
import chalk from 'chalk';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableVisibility,
} from '../graphql/generated';
import { DefaultEnvironment } from '../build/utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentSecretType, EnvironmentVariableVisibility } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import { RequestedPlatform } from '../platform';
import { promptAsync, selectAsync } from '../prompts';
const DEFAULT_ENVIRONMENTS = Object.values(DefaultEnvironment);
export async function getProjectEnvironmentVariableEnvironmentsAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string
): Promise<string[]> {
try {
const environments = await EnvironmentVariablesQuery.environmentVariableEnvironmentsAsync(
graphqlClient,
projectId
);
return environments;
} catch {
throw new Error('Failed to fetch available environments');
}
}
const CUSTOM_ENVIRONMENT_VALUE = '~~CUSTOM~~';
export async function promptVariableTypeAsync(
nonInteractive: boolean,
initialType?: EnvironmentSecretType
@@ -46,6 +65,24 @@ export function parseVisibility(
}
}
async function promptCustomEnvironmentAsync(): Promise<string> {
const { customEnvironment } = await promptAsync({
type: 'text',
name: 'customEnvironment',
message: 'Enter custom environment name:',
validate: (value: string) => {
if (!value || value.trim() === '') {
return 'Environment name cannot be empty';
}
if (!value.match(/^[a-zA-Z0-9_-]+$/)) {
return 'Environment name may only contain letters, numbers, underscores, and hyphens';
}
return true;
},
});
return customEnvironment;
}
export async function promptVariableVisibilityAsync(
nonInteractive: boolean,
selectedVisibility?: EnvironmentVariableVisibility | null
@@ -76,49 +113,93 @@ export async function promptVariableVisibilityAsync(
type EnvironmentPromptArgs = {
nonInteractive: boolean;
selectedEnvironments?: EnvironmentVariableEnvironment[];
availableEnvironments?: EnvironmentVariableEnvironment[];
selectedEnvironments?: string[];
graphqlClient?: ExpoGraphqlClient;
projectId?: string;
canEnterCustomEnvironment?: boolean;
};
export function promptVariableEnvironmentAsync(
input: EnvironmentPromptArgs & { multiple: true }
): Promise<EnvironmentVariableEnvironment[]>;
): Promise<string[]>;
export function promptVariableEnvironmentAsync(
input: EnvironmentPromptArgs & { multiple?: false }
): Promise<EnvironmentVariableEnvironment>;
): Promise<string>;
export async function promptVariableEnvironmentAsync({
nonInteractive,
selectedEnvironments,
multiple = false,
availableEnvironments,
}: EnvironmentPromptArgs & { multiple?: boolean }): Promise<
EnvironmentVariableEnvironment[] | EnvironmentVariableEnvironment
> {
canEnterCustomEnvironment = false,
graphqlClient,
projectId,
}: EnvironmentPromptArgs & { multiple?: boolean }): Promise<string[] | string> {
if (nonInteractive) {
throw new Error(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
}
if (!multiple) {
return await selectAsync(
'Select environment:',
(availableEnvironments ?? Object.values(EnvironmentVariableEnvironment)).map(environment => ({
title: environment.toLocaleLowerCase(),
value: environment,
}))
let allEnvironments: string[] = DEFAULT_ENVIRONMENTS;
if (graphqlClient && projectId) {
const projectEnvironments = await getProjectEnvironmentVariableEnvironmentsAsync(
graphqlClient,
projectId
);
allEnvironments = [...new Set([...DEFAULT_ENVIRONMENTS, ...projectEnvironments])];
}
if (!multiple) {
const choices = allEnvironments.map(environment => ({
title: environment,
value: environment,
}));
if (canEnterCustomEnvironment) {
choices.push({
title: 'Other (enter custom environment)',
value: CUSTOM_ENVIRONMENT_VALUE,
});
}
const selectedEnvironment = await selectAsync('Select environment:', choices);
if (selectedEnvironment === CUSTOM_ENVIRONMENT_VALUE) {
return await promptCustomEnvironmentAsync();
}
return selectedEnvironment;
}
const choices = allEnvironments.map(environment => ({
title: environment,
value: environment,
selected: selectedEnvironments?.includes(environment),
}));
if (canEnterCustomEnvironment) {
choices.push({
title: 'Other (enter custom environment)',
value: CUSTOM_ENVIRONMENT_VALUE,
selected: false,
});
}
const { environments } = await promptAsync({
message: 'Select environment:',
name: 'environments',
type: 'multiselect',
choices: Object.values(EnvironmentVariableEnvironment).map(environment => ({
title: environment.toLocaleLowerCase(),
value: environment,
selected: selectedEnvironments?.includes(environment),
})),
choices,
});
if (environments?.includes(CUSTOM_ENVIRONMENT_VALUE)) {
const customEnvironment = await promptCustomEnvironmentAsync();
const filteredEnvironments = environments.filter(
(env: string) => env !== CUSTOM_ENVIRONMENT_VALUE
);
return [...filteredEnvironments, customEnvironment];
}
return environments;
}
@@ -203,3 +284,30 @@ export async function promptVariableNameAsync(
return name;
}
export async function promptPlatformAsync({
message,
}: {
message: string;
}): Promise<RequestedPlatform> {
const { platform } = await promptAsync({
type: 'select',
message,
name: 'platform',
choices: [
{
title: 'All',
value: RequestedPlatform.All,
},
{
title: 'iOS',
value: RequestedPlatform.Ios,
},
{
title: 'Android',
value: RequestedPlatform.Android,
},
],
});
return platform;
}
+2 -9
View File
@@ -3,19 +3,12 @@ import dateFormat from 'dateformat';
import formatFields from './formatFields';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../graphql/generated';
import { EnvironmentVariableWithFileContent } from '../graphql/queries/EnvironmentVariablesQuery';
export function isEnvironment(environment: string): environment is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
environment as EnvironmentVariableEnvironment
);
}
export function formatVariableName(variable: EnvironmentVariableFragment): string {
const name = variable.name;
const scope = variable.scope === EnvironmentVariableScope.Project ? 'project' : 'shared';
@@ -53,8 +46,8 @@ export function formatVariableValue(variable: EnvironmentVariableWithFileContent
}
export async function performForEnvironmentsAsync(
environments: EnvironmentVariableEnvironment[] | null,
fun: (environment: EnvironmentVariableEnvironment | undefined) => Promise<any>
environments: string[] | null,
fun: (environment: string | undefined) => Promise<any>
): Promise<any[]> {
const selectedEnvironments = environments ?? [undefined];
return await Promise.all(selectedEnvironments.map(env => fun(env)));
+1 -18
View File
@@ -2,9 +2,8 @@ import { CombinedError } from '@urql/core';
import fs from 'fs';
import path from 'path';
import { getProjectGitHubSettingsUrl } from '../build/utils/url';
import { WorkflowRevisionMutation } from '../graphql/mutations/WorkflowRevisionMutation';
import Log, { link } from '../log';
import Log from '../log';
export namespace WorkflowFile {
export async function readWorkflowFileContentsAsync({
@@ -39,12 +38,8 @@ export namespace WorkflowFile {
export function maybePrintWorkflowFileValidationErrors({
error,
accountName,
projectName,
}: {
error: CombinedError;
accountName: string;
projectName: string;
}): void {
const validationErrors = error.graphQLErrors.flatMap(e => {
return WorkflowRevisionMutation.ValidationErrorExtensionZ.safeParse(e.extensions).data ?? [];
@@ -62,18 +57,6 @@ export namespace WorkflowFile {
}
}
}
const githubNotFoundError = error.graphQLErrors.find(
e => e.extensions.errorCode === 'GITHUB_NOT_FOUND_ERROR'
);
if (githubNotFoundError) {
Log.error(`GitHub repository not found. It is currently required to run workflows.`);
Log.error(
`Please check that the repository exists and that you have access to it. ${link(
getProjectGitHubSettingsUrl(accountName, projectName)
)}`
);
}
}
export function validateYamlExtension(fileName: string): void {
+30 -2
View File
@@ -9,9 +9,10 @@ import { pipeline } from 'node:stream/promises';
import { pack } from 'tar-stream';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
const EXPO_ROUTES_PATHS = new Set(['_expo/routes.json', '_expo/.routes.json', '.expo-routes.json']);
/** Returns whether a file or folder is ignored */
function isIgnoredName(name: string): boolean {
switch (name) {
@@ -114,6 +115,8 @@ export async function collectAssetsAsync(
throw new Error(
`Upload of "${file.normalizedPath}" aborted: File size is greater than the upload limit (>500MB)`
);
} else if (EXPO_ROUTES_PATHS.has(file.normalizedPath)) {
continue;
}
const sha512$ = computeSha512HashAsync(file.path);
const contentType$ = determineMimeTypeAsync(file.path);
@@ -129,6 +132,31 @@ export async function collectAssetsAsync(
return assets;
}
export interface RoutesConfigEntry {
headers?: Record<string, unknown>;
redirects?: Record<string, unknown>[];
}
export async function getRoutesConfigAsync(
assetPath: string | undefined
): Promise<RoutesConfigEntry | null> {
if (assetPath) {
for (const candidatePath of EXPO_ROUTES_PATHS) {
const targetPath = path.resolve(assetPath, candidatePath);
let json: unknown;
try {
json = JSON.parse(await fs.promises.readFile(targetPath, 'utf8'));
} catch {
continue;
}
if (typeof json === 'object' && json) {
return json as RoutesConfigEntry;
}
}
}
return null;
}
/** Mapping of normalized file paths to a SHA512 hash */
export type AssetMap = Record<
string,
@@ -162,7 +190,7 @@ export interface CreateManifestResult {
interface CreateManifestParams {
projectId: string;
projectDir: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
}
/** Creates a manifest configuration sent up for deployment */
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "16.18.0",
"version": "16.23.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/eas-build-job": "1.0.173",
"@expo/eas-build-job": "1.0.231",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "11.2.0",
+4 -2
View File
@@ -214,7 +214,8 @@
"environment": {
"description": "Environment to use for the server-side defined EAS environment variables during build process and command execution. Learn more: https://docs.expo.dev/eas/environment-variables/",
"markdownDescription": "Environment to use for the server-side defined EAS environment variables during build process and command execution. [Learn more](https://docs.expo.dev/eas/environment-variables/)",
"enum": ["production", "preview", "development"]
"examples": ["production", "preview", "development"],
"type": "string"
},
"android": {
"$ref": "#/definitions/BuildProfileAndroid"
@@ -512,7 +513,8 @@
"markdownDescription": "Path to the JSON file with service account key used to authenticate with Google Play. [Learn more](https://expo.fyi/creating-google-service-account)"
},
"track": {
"enum": ["beta", "alpha", "internal", "production"],
"type": "string",
"examples": ["beta", "alpha", "internal", "production"],
"description": "The track of the application to use. Learn more: https://support.google.com/googleplay/android-developer/answer/9859348?hl=en",
"markdownDescription": "The [track of the application](https://support.google.com/googleplay/android-developer/answer/9859348?hl=en) to use.",
"markdownEnumDescriptions": [
@@ -129,6 +129,35 @@ test('valid eas.json with specified environment', async () => {
});
});
test('supports custom environment names', async () => {
await fs.writeJson('/project/eas.json', {
build: {
development: {
environment: 'my-custom-environment',
},
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'development');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'development'
);
expect(androidProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
environment: 'my-custom-environment',
});
expect(iosProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
environment: 'my-custom-environment',
});
});
test('valid eas.json with specified keystoreName', async () => {
const keystoreName = 'this-is-a-keystore-name';
await fs.writeJson('/project/eas.json', {
+9 -1
View File
@@ -71,7 +71,15 @@ const CommonBuildProfileSchema = Joi.object({
// credentials
withoutCredentials: Joi.boolean(),
environment: Joi.string().valid('preview', 'production', 'development'),
environment: Joi.string()
.min(3)
.max(100)
.pattern(/^[a-z0-9_-]+$/)
.messages({
'string.min': 'Environment name must be at least 3 characters.',
'string.max': 'Environment name must not be longer than 100 characters.',
'string.pattern.base': 'Environment name can only contain lowercase letters, numbers, dashes and underscores.',
}),
});
const PlatformBuildProfileSchema = CommonBuildProfileSchema.concat(
+1 -1
View File
@@ -71,7 +71,7 @@ export interface CommonBuildProfile {
// credentials
withoutCredentials?: boolean;
environment?: 'preview' | 'production' | 'development';
environment?: string;
}
interface PlatformBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
+1 -1
View File
@@ -1,4 +1,4 @@
export { AndroidReleaseStatus, AndroidReleaseTrack, SubmitProfile } from './submit/types';
export { AndroidReleaseStatus, SubmitProfile } from './submit/types';
export { getDefaultProfile as getDefaultSubmitProfile } from './submit/resolver';
export { EasJson, ProfileType, AppVersionSource } from './types';
export { Platform } from '@expo/eas-build-job';
+2 -4
View File
@@ -1,12 +1,10 @@
import Joi from 'joi';
import { AndroidReleaseStatus, AndroidReleaseTrack } from './types';
import { AndroidReleaseStatus } from './types';
export const AndroidSubmitProfileSchema = Joi.object({
serviceAccountKeyPath: Joi.string(),
track: Joi.string()
.valid(...Object.values(AndroidReleaseTrack))
.default(AndroidReleaseTrack.internal),
track: Joi.string().default('internal'),
releaseStatus: Joi.string()
.valid(...Object.values(AndroidReleaseStatus))
.default(AndroidReleaseStatus.completed),
+1 -8
View File
@@ -7,16 +7,9 @@ export enum AndroidReleaseStatus {
inProgress = 'inProgress',
}
export enum AndroidReleaseTrack {
production = 'production',
beta = 'beta',
alpha = 'alpha',
internal = 'internal',
}
export interface AndroidSubmitProfile {
serviceAccountKeyPath?: string;
track: AndroidReleaseTrack;
track: string;
releaseStatus: AndroidReleaseStatus;
changesNotSentForReview: boolean;
applicationId?: string;
Generated
+30 -20
View File
@@ -1539,15 +1539,25 @@
slugify "^1.3.4"
sucrase "3.35.0"
"@expo/[email protected].173":
version "1.0.173"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.173.tgz#e19e0563743cc947fb5419fbeb063f03aed2a796"
integrity sha512-OtayVwXQaQEjJkpeKYvB4N6lsXVX+QSEz5okNx/d+8esI2KCjxOy/hzaqKBTovkxRlmWgE3XLw8RhrjAaBY2Gw==
"@expo/[email protected].231":
version "1.0.231"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.231.tgz#5c96045276da1e521794523204e9da6051e4dda1"
integrity sha512-jktsZ7IJYcC9lbgUWftbFCpfrhcGwJX4q0qSubUrqvFTMuUyLjCzJqyIZ4znbXLXfJSUPDJNAd4H0wBqKJA9kA==
dependencies:
"@expo/logger" "1.0.117"
"@expo/logger" "1.0.221"
joi "^17.13.1"
semver "^7.6.2"
zod "^3.23.8"
zod "^4.1.3"
"@expo/[email protected]":
version "1.0.237"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.237.tgz#2a3230540ac85599ee51d01b62c9d7b74ccfad2a"
integrity sha512-bvE7/+QO6hcyeD7Jn5job4jRZJNLep7MDaOsYTZ66xRNYiqY8g2TMKTcg8oOaBvhv8i5WJMJOexNmzkxWFjUtw==
dependencies:
"@expo/logger" "1.0.221"
joi "^17.13.1"
semver "^7.6.2"
zod "^4.1.3"
"@expo/env@^1.0.0":
version "1.0.0"
@@ -1602,10 +1612,10 @@
"@babel/code-frame" "~7.10.4"
json5 "^2.2.3"
"@expo/[email protected].117":
version "1.0.117"
resolved "https://registry.yarnpkg.com/@expo/logger/-/logger-1.0.117.tgz#31e302656f33dd015fdde89ad803d710a0e5b8a2"
integrity sha512-tuCT5fmjRjED6HYAf4HgE+UbiHT3JYrkUzUv9FC769goxo4LuuewBNQiDUkeecQTha7aTS5aO2YHlfdipI8gzg==
"@expo/[email protected].221":
version "1.0.221"
resolved "https://registry.yarnpkg.com/@expo/logger/-/logger-1.0.221.tgz#bd6ef6e03b3c4bf0872843d8b2a83556892178c0"
integrity sha512-arNkhzq4Ludz9J7PIYWTVYXuT1bO/A8prR8HYAviKuF6fEZfitemNVriGSetQASOE8Vnv3lmBZv6YIwLiFuv/Q==
dependencies:
"@types/bunyan" "^1.8.11"
bunyan "^1.8.15"
@@ -1736,13 +1746,13 @@
dependencies:
cross-spawn "^7.0.3"
"@expo/[email protected].173":
version "1.0.173"
resolved "https://registry.yarnpkg.com/@expo/steps/-/steps-1.0.173.tgz#157bf104d3137012bf82b106d5bd0227fd6f67f1"
integrity sha512-XT8+FsZWIzwK+/TGnPrHe0HMQrzRGjTsjVDx8Yt922tL5TkX69XeB/twA0PcPWBLMxq+JiKtFm68yPTPqjIMNg==
"@expo/[email protected].231":
version "1.0.231"
resolved "https://registry.yarnpkg.com/@expo/steps/-/steps-1.0.231.tgz#28fece40d97abb4ccddfad2ea003fa98d414f08a"
integrity sha512-o7Wj6W32S4H4geWC0b3RL5finHmiPT+e8SMP4/4Il2R0oc9er8jLXaj1tQ3fTZvpnlqoMWcFn5fAvaGnxfYDPA==
dependencies:
"@expo/eas-build-job" "1.0.173"
"@expo/logger" "1.0.117"
"@expo/eas-build-job" "1.0.231"
"@expo/logger" "1.0.221"
"@expo/spawn-async" "^1.7.2"
arg "^5.0.2"
fs-extra "^11.2.0"
@@ -15630,7 +15640,7 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zod@^4.1.3:
version "4.1.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.8.tgz#54f024adc634dd185006a8415da2bdd495b9d92b"
integrity sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==