Compare commits

...
Author SHA1 Message Date
Kadi Kraman 65130c666e Format urls as links 2024-10-10 13:26:24 +01:00
Will Schurman a346af2869 [eas-cli] Unhide rollout-percentage flag on update publish command (#2608) 2024-10-07 11:03:32 -07:00
Szymon Dziedzic 7699573100 [eas-json] update eas.schema.json (#2606)
# Why

add new image to eas.schema.json

# How

add new image to eas.schema.json
2024-10-03 17:41:16 +02:00
Piotr Szeremeta 3f832765c2 Refactor env:unlink, add tests (#2591)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Update command to use multiple environments, polish the interface.

# How

* use `variable-name` and `variable-environment` to select the variable
* Use `multiselect` and multiple `environment` parameters to allow unlinking multiple envs

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

# Test Plan

Added tests
2024-10-01 11:03:30 +02:00
Piotr Szeremeta af16cbaaea Refactor env:list and add tests (#2588)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Allow selecting multiple environments to display variables from.

# How

* Use `multiselect`, allow specifying multiple `environment` parameters
* List variables from all environments

# Test Plan

Added tests
2024-10-01 11:00:32 +02:00
Piotr Szeremeta 0d384d1722 Refactor env:link, add tests (#2587)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Add tests, polish the interface

# How

* Use `variable-name` and `variable-environment` to select the variable
* More robust selector of variable to remove ambiguity 
* Use multiselect to clearly indicate which environments are linked and which are not
* Automatic `link` and `unlink` action

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

# Test Plan

Added tests
2024-10-01 10:57:27 +02:00
Piotr Szeremeta 58d2088bf7 Refactor env:update, add tests (#2586)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Rework the EnvironmentVariableUpdate function - user is able to alter name, and environments.

# How

* Added `--variable-name` and `--variable-environment` parameters to identify the variable to update
* Using parameters from `--name` and `--environment` to update the variable using the new `updateEnvironmentVariable` method.

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

# Test Plan

Added tests
2024-10-01 10:54:28 +02:00
Piotr Szeremeta c622014b64 Add tests for EnvironmentVariableDelete (#2585)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Improve the selector for the variable to delete, rename parameter `name` to `variable-name`. 

All commands that were using name parameter to identify a single variable will use variable-name and variable-environment instead. This change will enable user to distinguish between name and environment that is identifying variable that is acted upon, and name and environment that is a parameter for command.

For example, when the user wants to update variable TEST that has environment production and change its name and change its environment, they can use:

eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/KfcKByW8uA9rvjzBy0RA/e96c9c31-be76-401a-be0e-715a77b399d1.png)

# Test Plan

Added tests
2024-10-01 10:51:28 +02:00
Piotr Szeremeta 81feffa0c5 Add tests to EnvironmentVariableGet (#2584)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Polished the interface:

 - user is prompted for environment only when the name is ambiguous.
- changed `name` parameter to `variable-name`. 

All commands that were using `name` parameter to identify a single variable will use `variable-name` and `variable-environment` instead. This change will enable user to distinguish between `name` and `environment` that is identifying variable that is acted upon, and `name` and `environment` that is a parameter for command.

For example, when the user wants to update variable `TEST` that has environment `production` and change its name and change its environment, they can use:

`eas env:update --variable-name TEST --variable-environment production --name TEST-update --environment production --environment preview`

# Test Plan

Added tests
2024-10-01 10:48:28 +02:00
Piotr Szeremeta 9521cc45da Update EnvironmentVariableCreate for multiple environments, add tests (#2583)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Update `eas env:create` to accept multiple `--environment` parameters.

# How

* Use `EASMultiEnvironmentFlag` in `EnvironmentVariableCreate`
* Update variable display with `environment` and `updatedAt` fields

# Test Plan

Added tests
2024-10-01 10:45:31 +02:00
Piotr Szeremeta 1dfe24d48a Allow prompting for multiple environments (#2582)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525: Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

EnvVar will support having multiple environments, CLI should support this.

# How

* Add an optional parameter `multiple` to `promptVariableEnvironmentAsync`
* Update commands using `promptVariableEnvironmentAsync`
* Add a new flag `EASMultiEnvironmentFlag`

# Test Plan

Tested manually, tests will be added in the following PRs
2024-10-01 10:42:27 +02:00
Piotr Szeremeta 99765c1802 Update EnvironmentVariableMutation and associated commands (#2581)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525 Update EAS-CLI for new EnvVar features](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features)

Bring up to date `create` and `update` EnvVar mutations.

# How

* Use `updateEnvironmentVariable` operation instead of `createEnvironmentVariable` with an 'overwrite' option.
* Add `type` parameter
* Add `environment` parameter for shared variables

# Test Plan

Tested manually, tests will be added in the following PRs
2024-10-01 10:39:28 +02:00
Piotr Szeremeta de388cfd27 Update Environment Variables queries: (#2580)
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

[ENG-13525 Update EAS-CLI for new EnvVar features
](https://linear.app/expo/issue/ENG-13525/update-eas-cli-for-new-envvar-features) 

Update EnvVar queries - allow quering using environments.

# How

* Added optional `environment` parameter to queries
* Fix typing of the `--environment` parameter

# Test Plan

Tested manually
2024-10-01 10:36:06 +02:00
Will Schurman 37a68c5237 [ci] Migrate from CODEOWNERS to codemention (#2603) 2024-09-27 09:06:46 -07:00
Will Schurman 6823e4f66c [eas-cli] Disallow republishing an update that is being rolled-out (#2602) 2024-09-27 09:05:24 -07:00
Phil Pluckthun ffa2f034f9 refactor(eas-cli): Implement updated deployment upload flow (#2601)
* Update API functions for new upload endpoint and deploy result

* Fix listWorkerFilesAsync file type

* Add call to finalize deployment

* Update CHANGELOG

* Add retries to /deploy/finalize call

* Apply lints
2024-09-27 10:36:24 +01:00
Expo CI 8243f5f0cb update CHANGELOG.md 2024-09-27 05:22:24 +00:00
37 changed files with 2910 additions and 582 deletions

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
rules:
- patterns: ['**/*']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/build/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/commands/build/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/submit/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
- patterns: ['packages/eas-cli/src/commands/submit.ts']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela', 'radoslawkrzemien']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['EvanBacon', 'byCedric', 'wschurman']
- patterns: ['packages/eas-cli/src/commands/update/**']
mentions: ['EvanBacon', 'byCedric', 'wschurman']
- patterns: ['packages/eas-cli/schema/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/commands/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-json/**']
mentions: ['szdziedzic', 'khamilowicz', 'sjchmiela']
+14
View File
@@ -0,0 +1,14 @@
name: codemention
on:
pull_request_target:
types: [opened, synchronize, ready_for_review]
jobs:
codemention:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: tobyhs/[email protected]
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
+11
View File
@@ -10,6 +10,17 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Disallow republishing an update that is being rolled-out. ([#2602](https://github.com/expo/eas-cli/pull/2602) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Implement new `worker` deploy API flow. ([#2601](https://github.com/expo/eas-cli/pull/2601) by [@kitten](https://github.com/kitten)))
- Unhide rollout-percentage flag on update publish command. ([#2608](https://github.com/expo/eas-cli/pull/2608) by [@wschurman](https://github.com/wschurman))
## [12.5.1](https://github.com/expo/eas-cli/releases/tag/v12.5.1) - 2024-09-27
### 🐛 Bug fixes
- Support assigning dev domain names in non interactive mode. ([#2595](https://github.com/expo/eas-cli/pull/2595) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
-16
View File
@@ -1,16 +0,0 @@
* @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/build @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/submit @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/commands/submit.ts @szdziedzic @khamilowicz @sjchmiela
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric
packages/eas-json @szdziedzic @khamilowicz @sjchmiela
+415 -27
View File
@@ -1506,6 +1506,18 @@
"name": "environmentVariables",
"description": "Environment variables for an account",
"args": [
{
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filterNames",
"description": null,
@@ -1551,6 +1563,18 @@
"name": "environmentVariablesIncludingSensitive",
"description": "Environment variables for an account with decrypted secret values",
"args": [
{
"name": "environment",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filterNames",
"description": null,
@@ -22369,12 +22393,28 @@
"name": "environment",
"description": null,
"type": {
"kind": "NON_NULL",
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environments",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
},
"defaultValue": null,
@@ -22409,6 +22449,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentSecretType",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": null,
@@ -23013,6 +23065,26 @@
"description": null,
"fields": null,
"inputFields": [
{
"name": "environments",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
@@ -23041,6 +23113,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentSecretType",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": null,
@@ -26543,6 +26627,26 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environments",
"description": null,
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
@@ -26559,6 +26663,51 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "linkedEnvironments",
"description": null,
"args": [
{
"name": "appFullName",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appId",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
@@ -26591,6 +26740,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentSecretType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": null,
@@ -26610,7 +26775,20 @@
{
"name": "value",
"description": null,
"args": [],
"args": [
{
"name": "includeFileContent",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "SCALAR",
"name": "String",
@@ -27005,13 +27183,9 @@
"name": "environment",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
@@ -27070,13 +27244,9 @@
"name": "environment",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
@@ -27110,6 +27280,39 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateEnvironmentVariable",
"description": "Update an environment variable",
"args": [
{
"name": "environmentVariableData",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateEnvironmentVariableInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EnvironmentVariable",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -27226,6 +27429,26 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environments",
"description": null,
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
@@ -27242,6 +27465,51 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "linkedEnvironments",
"description": null,
"args": [
{
"name": "appFullName",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "appId",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
@@ -27290,6 +27558,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentSecretType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": null,
@@ -27309,7 +27593,20 @@
{
"name": "value",
"description": null,
"args": [],
"args": [
{
"name": "includeFileContent",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "SCALAR",
"name": "String",
@@ -34477,13 +34774,9 @@
"name": "environment",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
@@ -45639,6 +45932,101 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateEnvironmentVariableInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "environments",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EnvironmentVariableEnvironment",
"ofType": null
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentSecretType",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "visibility",
"description": null,
"type": {
"kind": "ENUM",
"name": "EnvironmentVariableVisibility",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateGitHubBuildTriggerInput",
+16 -7
View File
@@ -26,15 +26,24 @@ export const EasNonInteractiveAndJsonFlags = {
}),
};
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
};
export const EASEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
};
export const EASMultiEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>({
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
...EasEnvironmentFlagParameters,
multiple: true,
}),
};
@@ -0,0 +1,354 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import {
promptVariableEnvironmentAsync,
promptVariableNameAsync,
promptVariableValueAsync,
} from '../../../utils/prompts';
import EnvironmentVariableCreate from '../create';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvironmentVariableCreate, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const variableId = 'testId';
const testAccountId = 'test-account-id';
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(EnvironmentVariableMutation.createForAppAsync)
.mockImplementation(async (_client, input, _appId) => ({
...input,
id: variableId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
}));
jest
.mocked(EnvironmentVariableMutation.createSharedVariableAsync)
.mockImplementation(async (_client, input, _appId) => ({
...input,
id: variableId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
}));
jest
.mocked(EnvironmentVariableMutation.updateAsync)
.mockImplementation(async (_client, input) => ({
...input,
id: variableId,
name: 'VarName',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
}));
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockImplementation(async () => []);
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockImplementation(async () => []);
});
describe('in interactive mode', () => {
it('creates a project variable', async () => {
const command = new EnvironmentVariableCreate(
['--name', 'VarName', '--value', 'VarValue', '--environment', 'production'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testProjectId
);
});
it('updates an existing variable in the same environment', async () => {
const command = new EnvironmentVariableCreate(
['--name', 'VarName', '--value', 'VarValue', '--environment', 'production'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
const otherVariableId = 'otherId';
jest
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Project,
},
]);
// @ts-expect-error
jest.spyOn(command, 'promptForOverwriteAsync').mockResolvedValue(true);
jest
.mocked(EnvironmentVariableMutation.updateAsync)
// @ts-expect-error
.mockImplementation(async (_client, input) => ({
...input,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
}));
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
filterNames: ['VarName'],
});
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
});
it('creates a shared variable', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--scope',
'shared',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
'test-account-id'
);
});
it('throws if a shared variable already exists', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--scope',
'shared',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest
.mocked(EnvironmentVariablesQuery.sharedAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: 'otherId',
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
await expect(command.runAsync()).rejects.toThrow();
});
it('updates if a shared variable already exists and --force flag is set', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--force',
'--scope',
'shared',
],
mockConfig
);
const otherVariableId = 'otherId';
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest
.mocked(EnvironmentVariablesQuery.sharedAsync)
// @ts-expect-error
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
await command.runAsync();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
});
it('creates a shared variable and links it', async () => {
const command = new EnvironmentVariableCreate(
[
'--name',
'VarName',
'--value',
'VarValue',
'--environment',
'production',
'--environment',
'development',
'--scope',
'shared',
'--link',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariableMutation.createSharedVariableAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [
EnvironmentVariableEnvironment.Production,
EnvironmentVariableEnvironment.Development,
],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testAccountId
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'testId',
testProjectId,
EnvironmentVariableEnvironment.Development
);
});
it('prompts for missing arguments', async () => {
const command = new EnvironmentVariableCreate([], mockConfig);
jest.mocked(promptVariableNameAsync).mockImplementation(async () => 'VarName');
jest.mocked(promptVariableValueAsync).mockImplementation(async () => 'VarValue');
jest
.mocked(promptVariableEnvironmentAsync)
// @ts-expect-error
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(promptVariableNameAsync).toHaveBeenCalled();
expect(promptVariableValueAsync).toHaveBeenCalled();
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
graphqlClient,
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
testProjectId
);
});
});
});
@@ -0,0 +1,127 @@
import { Config } from '@oclif/core';
import {
EnvironmentVariableEnvironment,
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 EnvironmentVariableDelete from '../delete';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../log');
describe(EnvironmentVariableDelete, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
beforeEach(() => {
jest.resetAllMocks();
});
it('deletes a variable by name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Production],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'production',
'--non-interactive',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
});
it('prompts for variable selection when name is not provided', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableDelete([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.deleteAsync).toHaveBeenCalledWith(graphqlClient, variableId);
expect(Log.withTick).toHaveBeenCalledWith('️Deleted variable TEST_VARIABLE".');
});
it('throws an error when variable name is not found', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow('Variable "NON_EXISTENT_VARIABLE" not found.');
});
it('throws an error when multiple variables with the same name are found', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
{ id: '2', name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableDelete(['--variable-name', 'TEST_VARIABLE'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
'Multiple variables with name "TEST_VARIABLE" found. Please select the variable to delete interactively or run command with --variable-environment ENVIRONMENT option.'
);
});
it('cancels deletion when user does not confirm', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(promptAsync as jest.Mock).mockResolvedValue({ variable: mockVariables[0] });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(false);
const command = new EnvironmentVariableDelete(['--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrowErrorMatchingSnapshot();
expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,109 @@
import { Config } from '@oclif/core';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../../utils/prompts';
import EnvironmentVariableGet from '../get';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../utils/prompts');
describe(EnvironmentVariableGet, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const mockVariables = [
{
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
},
];
it('retrieves environment variables successfully', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
jest.spyOn(Log, 'log').mockImplementation(() => {});
const command = new EnvironmentVariableGet(['--variable-name', 'TEST_VAR_1'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: undefined,
filterNames: ['TEST_VAR_1'],
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
});
it('handles errors during retrieval', async () => {
const errorMessage =
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag";
const command = new EnvironmentVariableGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await expect(command.runAsync()).rejects.toThrow(errorMessage);
});
it('prompts for variable name and environment if the name is ambigous', async () => {
jest
.mocked(promptVariableEnvironmentAsync)
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
const command = new EnvironmentVariableGet([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
jest.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).mockResolvedValueOnce([
...mockVariables,
{
id: 'var2',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
},
]);
await command.runAsync();
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
expect(promptVariableNameAsync).toHaveBeenCalled();
});
});
@@ -0,0 +1,277 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableLink from '../link';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableLink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Linked variable ${chalk.bold('TEST_VARIABLE')} to project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('links a shared variable to the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvironmentVariableLink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('links a shared variable to the current project to a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
const command = new EnvironmentVariableLink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('uses --variable-environment to select the variable with ambigous name', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockImplementation(
(_client, { environment }) => {
return mockVariables.filter(v => v.environments.includes(environment));
}
);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
'other-id',
projectId,
EnvironmentVariableEnvironment.Production
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({ environments: mockVariables[0].environments });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableLink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableLink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableLink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -0,0 +1,162 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import EnvironmentVariableList from '../list';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
const mockVariables: EnvironmentVariableFragment[] = [
{
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
},
{
id: 'var2',
name: 'TEST_VAR_2',
value: 'value2',
environments: [EnvironmentVariableEnvironment.Development],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
},
];
describe(EnvironmentVariableList, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
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 EnvironmentVariableList([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists project environment variables in specified environments', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--environment', 'production'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Production,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists project environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--include-sensitive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: undefined,
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists shared environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(['--scope', 'shared'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
it('lists shared environment variables including sensitive values', async () => {
jest
.mocked(EnvironmentVariablesQuery.sharedWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);
const command = new EnvironmentVariableList(
['--include-sensitive', '--scope', 'shared'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
privateProjectConfig: { projectId: testProjectId },
});
await command.runAsync();
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
});
});
@@ -0,0 +1,232 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableUnlink from '../unlink';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableUnlink, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
const successMessage = (env: EnvironmentVariableEnvironment): string =>
`Unlinked variable ${chalk.bold('TEST_VARIABLE')} from project ${chalk.bold(
'@testuser/testpp'
)} in ${env.toLocaleLowerCase()}.`;
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('unlinks a shared variable from the current project in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'TEST_VARIABLE', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Development)
);
});
it('unlinks a shared variable from the current project in a specified environment', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Production],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'TEST_VARIABLE', '--environment', 'production', '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.run();
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Production)
);
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
filterNames: ['TEST_VARIABLE'],
});
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
});
it('prompts for variable selection when the name is ambigous', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
{
id: 'other-id',
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
linkedEnvironments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(
EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync as jest.Mock
).mockResolvedValue(mockVariables[0]);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
expect(Log.withTick).toHaveBeenCalledWith(
successMessage(EnvironmentVariableEnvironment.Preview)
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
it('uses environments from prompt to both link and unlink environments', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Preview],
linkedEnvironments: [EnvironmentVariableEnvironment.Preview],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync as jest.Mock).mockResolvedValue(
mockVariables[0]
);
(selectAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({
environments: [EnvironmentVariableEnvironment.Production],
});
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUnlink([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Production
);
expect(EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync).toHaveBeenCalledWith(
graphqlClient,
variableId,
projectId,
EnvironmentVariableEnvironment.Preview
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUnlink(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
"Shared variable NON_EXISTENT_VARIABLE doesn't exist"
);
});
});
@@ -0,0 +1,164 @@
import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
} from '../../../graphql/generated';
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import EnvironmentVariableUpdate from '../update';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
describe(EnvironmentVariableUpdate, () => {
const projectId = 'test-project-id';
const variableId = '1';
const graphqlClient = {};
const mockConfig = {} as unknown as Config;
const mockContext = {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
};
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('updates a project variable by selected name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUpdate(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--non-interactive',
'--value',
'new-value',
'--name',
'NEW_VARIABLE',
'--environment',
'production',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
});
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on project @testuser/testpp.`
);
});
it('updates a shared variable by selected name in non-interactive mode', async () => {
const mockVariables = [
{
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
const command = new EnvironmentVariableUpdate(
[
'--variable-name',
'TEST_VARIABLE',
'--variable-environment',
'development',
'--non-interactive',
'--value',
'new-value',
'--name',
'NEW_VARIABLE',
'--environment',
'production',
'--scope',
'shared',
],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
});
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on account testuser.`
);
});
it('prompts for variable selection when selected name is not provided', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
(promptAsync as jest.Mock).mockResolvedValue({ name: mockVariables[0].name });
(toggleConfirmAsync as jest.Mock).mockResolvedValue(true);
const command = new EnvironmentVariableUpdate([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ id: variableId })
);
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on project @testuser/testpp.`
);
});
it('throws an error when variable name is not found', async () => {
const mockVariables: never[] = [];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
const command = new EnvironmentVariableUpdate(
['--variable-name', 'NON_EXISTENT_VARIABLE'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
'Variable with name NON_EXISTENT_VARIABLE does not exist on project @testuser/testpp.'
);
});
});
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnvironmentVariableDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with --variable-name VARIABLE_NAME flag to fix the issue"`;
+164 -106
View File
@@ -3,12 +3,13 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EASVariableVisibilityFlag,
} from '../../commandUtils/flags';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
@@ -26,6 +27,7 @@ import {
promptVariableNameAsync,
promptVariableValueAsync,
} from '../../utils/prompts';
import { performForEnvironmentsAsync } from '../../utils/variableUtils';
type CreateFlags = {
name?: string;
@@ -34,7 +36,7 @@ type CreateFlags = {
force?: boolean;
visibility?: EnvironmentVariableVisibility;
scope?: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment;
environment?: EnvironmentVariableEnvironment[];
'non-interactive': boolean;
};
@@ -60,7 +62,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
}),
...EASVariableVisibilityFlag,
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -73,16 +75,18 @@ export default class EnvironmentVariableCreate extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariableCreate);
let {
const validatedFlags = this.validateFlags(flags);
const {
name,
value,
scope,
'non-interactive': nonInteractive,
environment,
environment: environments,
visibility,
link,
force,
} = this.validateFlags(flags);
} = await this.promptForMissingFlagsAsync(validatedFlags);
const {
privateProjectConfig: { projectId },
@@ -96,90 +100,74 @@ export default class EnvironmentVariableCreate extends EasCommand {
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
]);
if (!name) {
name = await promptVariableNameAsync(nonInteractive);
}
let overwrite = false;
visibility = visibility ?? EnvironmentVariableVisibility.Public;
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
hidden: visibility !== EnvironmentVariableVisibility.Public,
});
}
if (scope === EnvironmentVariableScope.Project) {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
filterNames: [name],
});
const existingVariable = existingVariables.find(variable => variable.name === name);
const existingVariable = existingVariables.find(
variable => !environments || variable.environments?.some(env => environments?.includes(env))
);
if (existingVariable) {
if (existingVariable.scope === EnvironmentVariableScope.Project) {
await this.promptForOverwriteAsync({
nonInteractive,
force,
message: `Variable ${name} already exists on this project.`,
suggestion: 'Do you want to overwrite it?',
});
overwrite = true;
}
if (existingVariable.scope === EnvironmentVariableScope.Shared) {
if (!nonInteractive) {
const confirmation = await confirmAsync({
message: `Shared variable ${name} already exists on this project. Do you want to unlink it first?`,
});
await this.promptForOverwriteAsync({
nonInteractive,
force,
message: `Shared variable with ${name} name already exists on this account.`,
suggestion: 'Do you want to unlink it first?',
});
if (!confirmation) {
Log.log('Aborting');
throw new Error(`Shared variable ${name} already exists on this project.`);
}
} else if (!force) {
throw new Error(
`Shared variable ${name} already exists on this project. Use --force to overwrite it.`
);
}
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
existingVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinking shared variable ${chalk.bold(name)} on project ${chalk.bold(
projectDisplayName
)}.`
);
} else {
if (!nonInteractive) {
const confirmation = await confirmAsync({
message: `Variable ${name} already exists on this project. Do you want to overwrite it?`,
});
if (!confirmation) {
Log.log('Aborting');
throw new Error(
`Variable ${name} already exists on this project. Use --force to overwrite it.`
);
}
} else if (!force) {
throw new Error(
`Variable ${name} already exists on this project. Use --force to overwrite it.`
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
existingVariable.id,
projectId,
environment
);
}
overwrite = true;
});
}
}
const variable = await EnvironmentVariableMutation.createForAppAsync(
graphqlClient,
{
name,
value,
environment,
visibility,
overwrite,
},
projectId
);
const variable =
overwrite && existingVariable
? await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: existingVariable.id,
name,
value,
visibility,
environments,
})
: await EnvironmentVariableMutation.createForAppAsync(
graphqlClient,
{
name,
value,
environments,
visibility,
type: EnvironmentSecretType.String,
},
projectId
);
if (!variable) {
throw new Error(
`Could not create variable with name ${name} on project ${projectDisplayName}`
@@ -190,41 +178,46 @@ export default class EnvironmentVariableCreate extends EasCommand {
`Created a new variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
);
} else if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
const existingVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
filterNames: [name],
});
const existingVariable = sharedVariables.find(variable => variable.name === name);
const existingVariable = existingVariables.find(
variable => !environments || variable.environments?.some(env => environments?.includes(env))
);
if (existingVariable) {
throw new Error(
`Shared variable with ${name} name already exists on this account.\n` +
`Use a different name or delete the existing variable on website or by using eas env:delete --name ${name} --scope shared command.`
);
}
if (environment && !link) {
const confirmation = await confirmAsync({
message: `Unexpected argument: --environment can only be used with --link flag. Do you want to link the variable to the current project?`,
});
if (!confirmation) {
Log.log('Aborting');
throw new Error('Unexpected argument: --environment can only be used with --link flag.');
if (force) {
overwrite = true;
} else {
throw new Error(
`Shared variable with ${name} name already exists on this account.\n` +
`Use a different name or delete the existing variable on website or by using eas env:delete --name ${name} --scope shared command.`
);
}
}
if (!environment && link) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const variable = await EnvironmentVariableMutation.createSharedVariableAsync(
graphqlClient,
{
name,
value,
visibility,
},
ownerAccount.id
);
const variable =
overwrite && existingVariable
? await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: existingVariable.id,
name,
value,
visibility,
environments,
})
: await EnvironmentVariableMutation.createSharedVariableAsync(
graphqlClient,
{
name,
value,
visibility,
environments,
type: EnvironmentSecretType.String,
},
ownerAccount.id
);
if (!variable) {
throw new Error(
@@ -236,18 +229,22 @@ export default class EnvironmentVariableCreate extends EasCommand {
`Created a new variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
);
if (link && environment) {
if (link) {
Log.withTick(
`Linking shared variable ${chalk.bold(name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
variable.id,
projectId,
environment
);
await performForEnvironmentsAsync(environments, async environment => {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
variable.id,
projectId,
environment
);
});
Log.withTick(
`Linked shared variable ${chalk.bold(name)} to project ${chalk.bold(projectDisplayName)}.`
);
@@ -255,6 +252,67 @@ export default class EnvironmentVariableCreate extends EasCommand {
}
}
private async promptForOverwriteAsync({
nonInteractive,
force,
message,
suggestion,
}: {
nonInteractive: boolean;
force: boolean;
message: string;
suggestion: string;
}): Promise<void> {
if (!nonInteractive) {
const confirmation = await confirmAsync({
message: `${message} ${suggestion}`,
});
if (!confirmation) {
Log.log('Aborting');
throw new Error(`${message}`);
}
} else if (!force) {
throw new Error(`${message} Use --force to overwrite it.`);
}
}
private async promptForMissingFlagsAsync({
name,
value,
environment,
visibility = EnvironmentVariableVisibility.Public,
'non-interactive': nonInteractive,
...rest
}: CreateFlags): Promise<Required<CreateFlags>> {
if (!name) {
name = await promptVariableNameAsync(nonInteractive);
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
hidden: visibility !== EnvironmentVariableVisibility.Public,
});
}
if (!environment) {
environment = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
}
return {
name,
value,
environment,
visibility,
link: rest.link ?? false,
force: rest.force ?? false,
scope: rest.scope ?? EnvironmentVariableScope.Project,
'non-interactive': nonInteractive,
...rest,
};
}
private validateFlags(flags: CreateFlags): CreateFlags {
if (flags.scope !== EnvironmentVariableScope.Shared && flags.link) {
throw new Error(
+50 -35
View File
@@ -1,22 +1,23 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment, 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 { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
type DeleteFlags = {
name?: string;
environment?: EnvironmentVariableEnvironment;
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
scope?: EnvironmentVariableScope;
};
@@ -27,11 +28,14 @@ export default class EnvironmentVariableDelete extends EasCommand {
static override hidden = true;
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable to delete',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to delete',
}),
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -42,7 +46,12 @@ export default class EnvironmentVariableDelete extends EasCommand {
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariableDelete);
let { name, environment, 'non-interactive': nonInteractive, scope } = this.validateFlags(flags);
const {
'variable-name': name,
'variable-environment': environment,
'non-interactive': nonInteractive,
scope,
} = this.validateFlags(flags);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
@@ -50,47 +59,53 @@ export default class EnvironmentVariableDelete extends EasCommand {
nonInteractive,
});
if (scope === EnvironmentVariableScope.Project) {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
}
const variables =
scope === EnvironmentVariableScope.Project && environment
scope === EnvironmentVariableScope.Project
? await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
})
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId });
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment,
});
let selectedVariable;
if (!name) {
({ name } = await promptAsync({
({ variable: selectedVariable } = await promptAsync({
type: 'select',
name: 'name',
name: 'variable',
message: 'Pick the variable to be deleted:',
choices: variables
.filter(({ scope: variableScope }) => scope === variableScope)
.map(variable => ({
title: variable.name,
value: variable.name,
})),
.map(variable => {
return {
title: formatVariableName(variable),
value: variable,
};
}),
}));
} else {
const selectedVariables = variables.filter(
variable =>
variable.name === name && (!environment || variable.environments?.includes(environment))
);
if (!name) {
throw new Error(
`Environment variable wasn't selected. Run the command again and select existing variable or run it with ${chalk.bold(
'--name VARIABLE_NAME'
)} flag to fix the issue.`
);
if (selectedVariables.length !== 1) {
if (selectedVariables.length === 0) {
throw new Error(`Variable "${name}" not found.`);
} else {
throw new Error(
`Multiple variables with name "${name}" found. Please select the variable to delete interactively or run command with --variable-environment ENVIRONMENT option.`
);
}
}
selectedVariable = selectedVariables[0];
}
const selectedVariable = variables.find(variable => variable.name === name);
if (!selectedVariable) {
throw new Error(`Variable "${name}" not found.`);
}
assert(selectedVariable, `Variable "${name}" not found.`);
if (!nonInteractive) {
Log.addNewLineIfNone();
@@ -106,7 +121,7 @@ export default class EnvironmentVariableDelete extends EasCommand {
});
if (!confirmed) {
Log.error(`Canceled deletion of variable ${selectedVariable.name}.`);
throw new Error(`Variable "${name}" not deleted.`);
throw new Error(`Variable "${selectedVariable.name}" not deleted.`);
}
}
@@ -117,10 +132,10 @@ export default class EnvironmentVariableDelete extends EasCommand {
private validateFlags(flags: DeleteFlags): DeleteFlags {
if (flags['non-interactive']) {
if (!flags.name) {
if (!flags['variable-name']) {
throw new Error(
`Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with ${chalk.bold(
'--name VARIABLE_NAME'
'--variable-name VARIABLE_NAME'
)} flag to fix the issue`
);
}
+2 -1
View File
@@ -64,7 +64,8 @@ export default class EnvExec extends EasCommand {
});
const environment =
parsedFlags.environment ?? (await promptVariableEnvironmentAsync(parsedFlags.nonInteractive));
parsedFlags.environment ??
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
const environmentVariables = await this.loadEnvironmentVariablesAsync({
graphqlClient,
projectId,
+56 -34
View File
@@ -1,13 +1,14 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import {
EnvironmentVariableEnvironment,
@@ -16,12 +17,12 @@ import {
} from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { formatVariable } from '../../utils/formatVariable';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
import { formatVariable } from '../../utils/variableUtils';
type GetFlags = {
name?: string;
environment?: EnvironmentVariableEnvironment;
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
format?: string;
scope: EnvironmentVariableScope;
@@ -38,21 +39,24 @@ export default class EnvironmentVariableGet extends EasCommand {
};
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable',
}),
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASNonInteractiveFlag,
...EASEnvironmentFlag,
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(EnvironmentVariableGet);
let {
environment,
name,
'variable-environment': environment,
'variable-name': name,
'non-interactive': nonInteractive,
format,
scope,
@@ -69,15 +73,41 @@ export default class EnvironmentVariableGet extends EasCommand {
name = await promptVariableNameAsync(nonInteractive);
}
if (!environment && scope === EnvironmentVariableScope.Project) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const variable = await getVariableAsync(graphqlClient, scope, projectId, name, environment);
const variables = await getVariablesAsync(graphqlClient, scope, projectId, name, environment);
if (!variable) {
if (variables.length === 0) {
Log.error(`Variable with name "${name}" not found`);
return;
}
let variable;
if (variables.length > 1) {
if (!environment) {
const availableEnvironments = variables.reduce<EnvironmentVariableEnvironment[]>(
(acc, v) => [...acc, ...(v.environments ?? [])],
[] as EnvironmentVariableEnvironment[]
);
environment = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: false,
availableEnvironments,
});
}
assert(environment, 'Environment is required.');
const variableInEnvironment = variables.find(v => v.environments?.includes(environment!));
if (!variableInEnvironment) {
throw new Error(`Variable with name "${name}" not found in environment "${environment}"`);
}
variable = variableInEnvironment;
} else {
variable = variables[0];
}
if (!variable.value) {
throw new Error(
`${chalk.bold(
@@ -94,17 +124,14 @@ export default class EnvironmentVariableGet extends EasCommand {
}
private validateFlags(flags: GetFlags): GetFlags {
if (flags.environment && flags.scope === EnvironmentVariableScope.Shared) {
throw new Error(`Unexpected argument: --environment can only be used with project variables`);
}
if (flags['non-interactive']) {
if (!flags.name) {
throw new Error('Variable name is required. Run the command with --name flag.');
if (!flags['variable-name']) {
throw new Error('Variable name is required. Run the command with --variable-name flag.');
}
if (!flags.scope) {
throw new Error('Scope is required. Run the command with --scope flag.');
}
if (!flags.environment && flags.scope === EnvironmentVariableScope.Project) {
if (!flags['variable-environment']) {
throw new Error('Environment is required.');
}
}
@@ -112,29 +139,26 @@ export default class EnvironmentVariableGet extends EasCommand {
}
}
async function getVariableAsync(
async function getVariablesAsync(
graphqlClient: ExpoGraphqlClient,
scope: string,
projectId: string,
name: string | undefined,
environment: string | undefined
): Promise<EnvironmentVariableFragment | null> {
if (!environment && scope === EnvironmentVariableScope.Project) {
throw new Error('Environment is required.');
}
environment: EnvironmentVariableEnvironment | undefined
): Promise<EnvironmentVariableFragment[]> {
if (!name) {
throw new Error("Variable name is required. Run the command with '--name VARIABLE_NAME' flag.");
throw new Error(
"Variable name is required. Run the command with '--variable-name VARIABLE_NAME' flag."
);
}
if (environment && scope === EnvironmentVariableScope.Project) {
if (scope === EnvironmentVariableScope.Project) {
const appVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
filterNames: [name],
});
return appVariables[0];
}
if (scope === EnvironmentVariableScope.Shared) {
return appVariables;
} else {
const sharedVariables = await EnvironmentVariablesQuery.sharedWithSensitiveAsync(
graphqlClient,
{
@@ -142,8 +166,6 @@ async function getVariableAsync(
filterNames: [name],
}
);
return sharedVariables[0];
return sharedVariables;
}
return null;
}
+94 -26
View File
@@ -2,13 +2,19 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import {
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
export default class EnvironmentVariableLink extends EasCommand {
static override description = 'link a shared environment variable to the current project';
@@ -16,10 +22,14 @@ export default class EnvironmentVariableLink extends EasCommand {
static override hidden = true;
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable',
}),
...EASEnvironmentFlag,
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to link',
}),
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -30,7 +40,12 @@ export default class EnvironmentVariableLink extends EasCommand {
async runAsync(): Promise<void> {
let {
flags: { name, 'non-interactive': nonInteractive, environment },
flags: {
'variable-name': name,
'variable-environment': currentEnvironment,
'non-interactive': nonInteractive,
environment: environments,
},
} = await this.parse(EnvironmentVariableLink);
const {
privateProjectConfig: { projectId },
@@ -42,44 +57,97 @@ export default class EnvironmentVariableLink extends EasCommand {
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment: currentEnvironment,
filterNames: name ? [name] : undefined,
});
if (!name) {
name = await selectAsync(
let selectedVariable = variables[0];
if (variables.length > 1) {
selectedVariable = await selectAsync(
'Select shared variable',
variables.map(variable => ({
title: variable.name,
value: variable.name,
title: formatVariableName(variable),
value: variable,
}))
);
}
const selectedVariable = variables.find(variable => variable.name === name);
if (!selectedVariable) {
throw new Error(`Shared variable ${name} doesn't exist`);
}
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
let explicitSelect = false;
if (!nonInteractive && !environments) {
const selectedEnvironments =
(selectedVariable.linkedEnvironments ?? []).length > 0
? selectedVariable.linkedEnvironments
: selectedVariable.environments;
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedEnvironments ?? [],
});
explicitSelect = true;
}
const linkedVariable = await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
if (!linkedVariable) {
throw new Error(
`Could not link variable with name ${selectedVariable.name} to project with id ${projectId}`
if (!environments) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
);
return;
}
Log.withTick(
`Linked variable ${chalk.bold(linkedVariable.name)} to project ${chalk.bold(
projectDisplayName
)}.`
);
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
try {
if (
selectedVariable.linkedEnvironments?.includes(environment) ===
environments.includes(environment)
) {
if (!explicitSelect && environments.includes(environment)) {
Log.withTick(
`Variable ${chalk.bold(
selectedVariable.name
)} is already linked to ${environment.toLocaleLowerCase()}.`
);
}
continue;
}
if (environments.includes(environment)) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
} else if (explicitSelect) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
}
} catch (err: any) {
Log.warn(err.message);
}
}
}
}
+77 -63
View File
@@ -4,7 +4,7 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentFlag,
EASMultiEnvironmentFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
} from '../../commandUtils/flags';
@@ -15,8 +15,43 @@ import {
} from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { formatVariable } from '../../utils/formatVariable';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariable, performForEnvironmentsAsync } from '../../utils/variableUtils';
async function getVariablesForScopeAsync(
graphqlClient: ExpoGraphqlClient,
{
scope,
includingSensitive,
environment,
projectId,
}: {
scope: EnvironmentVariableScope;
includingSensitive: boolean;
environment?: EnvironmentVariableEnvironment;
projectId: string;
}
): Promise<EnvironmentVariableFragment[]> {
if (scope === EnvironmentVariableScope.Project) {
if (includingSensitive) {
return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return includingSensitive
? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
})
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId, environment });
}
export default class EnvironmentValueList extends EasCommand {
static override description = 'list environment variables for the current project';
@@ -35,12 +70,18 @@ export default class EnvironmentValueList extends EasCommand {
}),
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
};
async runAsync(): Promise<void> {
let {
flags: { environment, format, scope, 'include-sensitive': includeSensitive },
flags: {
environment: environments,
format,
scope,
'include-sensitive': includeSensitive,
'non-interactive': nonInteractive,
},
} = await this.parse(EnvironmentValueList);
const {
privateProjectConfig: { projectId },
@@ -49,70 +90,43 @@ export default class EnvironmentValueList extends EasCommand {
nonInteractive: true,
});
if (scope === EnvironmentVariableScope.Project && !environment) {
environment = await promptVariableEnvironmentAsync(false);
if (!environments) {
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
}
const variables = await this.getVariablesForScopeAsync(graphqlClient, {
scope,
includingSensitive: includeSensitive,
environment,
projectId,
});
await performForEnvironmentsAsync(environments, async environment => {
const variables = await getVariablesForScopeAsync(graphqlClient, {
scope,
includingSensitive: includeSensitive,
environment,
projectId,
});
if (format === 'short') {
for (const variable of variables) {
// TODO: Add Learn more link
Log.addNewLineIfNone();
if (environment) {
Log.log(chalk.bold(`Environment: ${environment}`));
}
if (format === 'short') {
for (const variable of variables) {
// TODO: Add Learn more link
Log.log(
`${chalk.bold(variable.name)}=${
variable.value ??
"***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)"
}`
);
}
} else {
if (scope === EnvironmentVariableScope.Shared) {
Log.log(chalk.bold('Shared variables for this account:'));
} else {
Log.log(chalk.bold(`Variables for this project:`));
}
Log.log(
`${chalk.bold(variable.name)}=${
variable.value ??
"***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)"
}`
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
);
}
} else {
if (scope === EnvironmentVariableScope.Shared) {
Log.log(chalk.bold('Shared variables for this account:'));
} else {
Log.log(chalk.bold(`Variables for this project for environment ${environment}:`));
}
Log.log(
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}
private async getVariablesForScopeAsync(
graphqlClient: ExpoGraphqlClient,
{
scope,
includingSensitive,
environment,
projectId,
}: {
scope: EnvironmentVariableScope;
includingSensitive: boolean;
environment?: EnvironmentVariableEnvironment;
projectId: string;
}
): Promise<EnvironmentVariableFragment[]> {
if (scope === EnvironmentVariableScope.Project && environment) {
if (includingSensitive) {
return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
});
}
return includingSensitive
? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, {
appId: projectId,
})
: await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId });
});
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ export default class EnvironmentVariablePull extends EasCommand {
} = await this.parse(EnvironmentVariablePull);
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
environment = await promptVariableEnvironmentAsync({ nonInteractive });
}
const {
privateProjectConfig: { projectId },
+90 -40
View File
@@ -2,14 +2,15 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableScope } from '../../graphql/generated';
import { EASMultiEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
export default class EnvironmentVariableUnlink extends EasCommand {
static override description = 'unlink a shared environment variable to the current project';
@@ -17,10 +18,10 @@ export default class EnvironmentVariableUnlink extends EasCommand {
static override hidden = true;
static override flags = {
name: Flags.string({
'variable-name': Flags.string({
description: 'Name of the variable',
}),
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -31,7 +32,11 @@ export default class EnvironmentVariableUnlink extends EasCommand {
async runAsync(): Promise<void> {
let {
flags: { name, 'non-interactive': nonInteractive, environment },
flags: {
'variable-name': name,
'non-interactive': nonInteractive,
environment: unlinkEnvironments,
},
} = await this.parse(EnvironmentVariableUnlink);
const {
privateProjectConfig: { projectId },
@@ -40,57 +45,102 @@ export default class EnvironmentVariableUnlink extends EasCommand {
nonInteractive,
});
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
const appVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
const variables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment,
filterNames: name ? [name] : undefined,
});
const linkedVariables = appVariables.filter(
({ scope }) => scope === EnvironmentVariableScope.Shared
);
if (linkedVariables.length === 0) {
throw new Error(`There are no linked shared env variables for project ${projectDisplayName}`);
}
let selectedVariable = variables[0];
if (!name) {
name = await selectAsync(
'Select shared variable to unlink',
linkedVariables.map(variable => ({
title: variable.name,
value: variable.name,
if (variables.length > 1) {
if (nonInteractive) {
throw new Error("Multiple variables found, please select one using '--variable-name'");
}
selectedVariable = await selectAsync(
'Select shared variable',
variables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
}
const selectedVariable = linkedVariables.find(variable => variable.name === name);
if (!selectedVariable) {
throw new Error(`Shared variable ${name} doesn't exist`);
}
const unlinkedVariable = await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
let explicitSelect = false;
if (!unlinkedVariable) {
throw new Error(
`Could not unlink variable with name ${selectedVariable.name} from project ${projectDisplayName}`
if (!nonInteractive && !unlinkEnvironments) {
const selectedEnvironments =
(selectedVariable.linkedEnvironments ?? []).length > 0
? selectedVariable.linkedEnvironments
: selectedVariable.environments;
const environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedEnvironments ?? [],
});
explicitSelect = true;
unlinkEnvironments = Object.values(EnvironmentVariableEnvironment).filter(
env => !environments.includes(env)
);
}
Log.withTick(
`Unlinked variable ${chalk.bold(unlinkedVariable.name)} from the project ${chalk.bold(
projectDisplayName
)}.`
);
if (!unlinkEnvironments) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${selectedVariable.environments?.join(', ').toLocaleLowerCase()}.`
);
return;
}
for (const environment of Object.values(EnvironmentVariableEnvironment)) {
if (
selectedVariable.linkedEnvironments?.includes(environment) !==
unlinkEnvironments.includes(environment)
) {
if (!explicitSelect && unlinkEnvironments.includes(environment)) {
Log.withTick(
`Variable ${chalk.bold(
selectedVariable.name
)} is already unlinked from ${environment.toLocaleLowerCase()}.`
);
}
continue;
}
if (unlinkEnvironments.includes(environment)) {
await EnvironmentVariableMutation.unlinkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Unlinked variable ${chalk.bold(selectedVariable.name)} from project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
} else if (explicitSelect) {
await EnvironmentVariableMutation.linkSharedEnvironmentVariableAsync(
graphqlClient,
selectedVariable.id,
projectId,
environment
);
Log.withTick(
`Linked variable ${chalk.bold(selectedVariable.name)} to project ${chalk.bold(
projectDisplayName
)} in ${environment.toLocaleLowerCase()}.`
);
}
}
}
}
+119 -122
View File
@@ -1,14 +1,21 @@
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableScopeFlag,
EASVariableVisibilityFlag,
EasEnvironmentFlagParameters,
} from '../../commandUtils/flags';
import { EnvironmentVariableScope, EnvironmentVariableVisibility } from '../../graphql/generated';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
@@ -17,14 +24,22 @@ import {
getOwnerAccountForProjectIdAsync,
} from '../../project/projectUtils';
import { selectAsync } from '../../prompts';
import { promptVariableEnvironmentAsync, promptVariableValueAsync } from '../../utils/prompts';
import {
promptVariableEnvironmentAsync,
promptVariableNameAsync,
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { formatVariableName } from '../../utils/variableUtils';
type UpdateFlags = {
name?: string;
value?: string;
scope?: EnvironmentVariableScope;
environment?: string;
environment?: EnvironmentVariableEnvironment[];
visibility?: EnvironmentVariableVisibility;
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'non-interactive': boolean;
};
@@ -35,15 +50,22 @@ export default class EnvironmentVariableUpdate extends EasCommand {
static override hidden = true;
static override flags = {
'variable-name': Flags.string({
description: 'Current name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to update',
}),
name: Flags.string({
description: 'Name of the variable',
description: 'New name of the variable',
}),
value: Flags.string({
description: 'Text value or the variable',
description: 'New value or the variable',
}),
...EASVariableVisibilityFlag,
...EASVariableScopeFlag,
...EASEnvironmentFlag,
...EASMultiEnvironmentFlag,
...EASNonInteractiveFlag,
};
@@ -59,8 +81,10 @@ export default class EnvironmentVariableUpdate extends EasCommand {
name,
value,
scope,
'variable-name': currentName,
'variable-environment': currentEnvironment,
'non-interactive': nonInteractive,
environment,
environment: environments,
visibility,
} = this.validateFlags(flags);
@@ -76,128 +100,101 @@ export default class EnvironmentVariableUpdate extends EasCommand {
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
]);
let selectedVariable: EnvironmentVariableFragment;
let existingVariables: EnvironmentVariableFragment[] = [];
const suffix =
scope === EnvironmentVariableScope.Project
? `on project ${projectDisplayName}`
: `on account ${ownerAccount.name}`;
if (scope === EnvironmentVariableScope.Project) {
if (!environment) {
environment = await promptVariableEnvironmentAsync(nonInteractive);
}
const existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
existingVariables = await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, {
appId: projectId,
environment,
environment: currentEnvironment,
filterNames: currentName ? [currentName] : undefined,
});
if (!name) {
name = await selectAsync(
'Select variable',
existingVariables.map(variable => ({
title: variable.name,
value: variable.name,
}))
);
}
const existingVariable = existingVariables.find(variable => variable.name === name);
if (!existingVariable) {
throw new Error(
`Variable with name ${name} does not exist on project ${projectDisplayName}`
);
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
required: false,
initial: existingVariable.value,
});
if (!value || value.length === 0) {
value = '';
}
}
const variable = await EnvironmentVariableMutation.createForAppAsync(
graphqlClient,
{
name,
value,
environment,
visibility,
overwrite: true,
},
projectId
);
if (!variable) {
throw new Error(
`Could not update variable with name ${name} on project ${projectDisplayName}`
);
}
Log.withTick(
`Updated variable ${chalk.bold(name)} on project ${chalk.bold(projectDisplayName)}.`
);
} else if (scope === EnvironmentVariableScope.Shared) {
const sharedVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
});
if (!name) {
name = await selectAsync(
'Select variable',
sharedVariables.map(variable => ({
title: variable.name,
value: variable.name,
}))
);
}
const existingVariable = sharedVariables.find(variable => variable.name === name);
if (!existingVariable) {
throw new Error(
"Variable with this name doesn't exist on this account. Use a different name."
);
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
required: false,
initial: existingVariable.value,
});
if (!value || value.length === 0) {
value = '';
}
}
const variable = await EnvironmentVariableMutation.createSharedVariableAsync(
graphqlClient,
{
name,
value,
visibility,
overwrite: true,
},
ownerAccount.id
);
if (!variable) {
throw new Error(
`Could not update variable with name ${name} on account ${ownerAccount.name}`
);
}
Log.withTick(
`Updated shared variable ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
);
}
if (scope === EnvironmentVariableScope.Shared) {
existingVariables = await EnvironmentVariablesQuery.sharedAsync(graphqlClient, {
appId: projectId,
environment: currentEnvironment,
filterNames: currentName ? [currentName] : undefined,
});
}
if (existingVariables.length === 0) {
throw new Error(
`Variable with name ${currentName} ${
currentEnvironment ? `in environment ${currentEnvironment}` : ''
} does not exist ${suffix}.`
);
} else if (existingVariables.length > 1) {
selectedVariable = await selectAsync(
'Select variable',
existingVariables.map(variable => ({
title: formatVariableName(variable),
value: variable,
}))
);
} else {
selectedVariable = existingVariables[0];
}
assert(selectedVariable, 'Variable must be selected');
if (!nonInteractive) {
if (!name) {
name = await promptVariableNameAsync(nonInteractive, selectedVariable.name);
if (!name || name.length === 0) {
name = undefined;
}
}
if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
required: false,
initial: selectedVariable.value,
});
if (!value || value.length === 0) {
value = undefined;
}
}
if (!environments || environments.length === 0) {
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
selectedEnvironments: selectedVariable.environments ?? [],
});
}
if (!visibility) {
visibility = await promptVariableVisibilityAsync(
nonInteractive,
selectedVariable.visibility
);
}
}
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: selectedVariable.id,
name,
value,
environments,
visibility,
});
if (!variable) {
throw new Error(`Could not update variable with name ${name} ${suffix}`);
}
Log.withTick(`Updated variable ${chalk.bold(selectedVariable.name)} ${suffix}.`);
}
private validateFlags(flags: UpdateFlags): UpdateFlags {
if (flags['non-interactive']) {
if (!flags.name) {
if (!flags['variable-name']) {
throw new Error(
'Variable name is required in non-interactive mode. Run the command with --name flag.'
);
}
if (flags.scope === EnvironmentVariableScope.Project && !flags.environment) {
throw new Error(
'Environment is required when updating project-wide variable in non-interactive mode. Run the command with --environment flag.'
'Current name is required in non-interactive mode. Run the command with --variable-name flag.'
);
}
}
@@ -127,7 +127,6 @@ export default class UpdatePublish extends EasCommand {
'rollout-percentage': Flags.integer({
description: `Percentage of users this update should be immediately available to. Users not in the rollout will be served the previous latest update on the branch, even if that update is itself being rolled out. The specified number must be an integer between 1 and 100. When not specified, this defaults to 100.`,
required: false,
hidden: true,
min: 0,
max: 100,
}),
+48 -15
View File
@@ -17,7 +17,12 @@ import {
assignWorkerDeploymentProductionAsync,
getSignedDeploymentUrlAsync,
} from '../../worker/deployment';
import { UploadParams, batchUploadAsync, uploadAsync } from '../../worker/upload';
import {
UploadParams,
batchUploadAsync,
callUploadApiAsync,
uploadAsync,
} from '../../worker/upload';
import {
formatWorkerDeploymentJson,
formatWorkerDeploymentTable,
@@ -50,6 +55,13 @@ interface RawDeployFlags {
'export-dir': string;
}
interface DeployInProgressParams {
id: string;
fullName: string;
baseURL: string;
token: string;
}
export default class WorkerDeploy extends EasCommand {
static override description = 'Deploy your Expo web build';
static override aliases = ['deploy'];
@@ -126,7 +138,24 @@ export default class WorkerDeploy extends EasCommand {
}
}
async function uploadTarballAsync(tarPath: string, uploadUrl: string): Promise<any> {
async function finalizeDeployAsync(deployParams: DeployInProgressParams): Promise<void> {
const finalizeDeployUrl = new URL('/deploy/finalize', deployParams.baseURL);
finalizeDeployUrl.searchParams.set('token', deployParams.token);
const result = await callUploadApiAsync(finalizeDeployUrl, {
method: 'POST',
headers: {
accept: 'application/json',
},
});
if (!result || typeof result !== 'object' || !('success' in result) || !result.success) {
throw new Error('Deploy failed: Incomplete asset uploads. Please try again');
}
}
async function uploadTarballAsync(
tarPath: string,
uploadUrl: string
): Promise<DeployInProgressParams> {
const { response } = await uploadAsync({
url: uploadUrl,
filePath: tarPath,
@@ -150,19 +179,23 @@ export default class WorkerDeploy extends EasCommand {
if (!json.success || !json.result || typeof json.result !== 'object') {
throw new Error(json.message ? `Upload failed: ${json.message}` : 'Upload failed!');
}
return json.result;
const { id, fullName, token } = json.result;
if (typeof token !== 'string') {
throw new Error('Upload failed: API failed to return a deployment token');
} else if (typeof id !== 'string') {
throw new Error('Upload failed: API failed to return a deployment identifier');
} else if (typeof fullName !== 'string') {
throw new Error('Upload failed: API failed to return a script name');
}
const baseURL = new URL('/', uploadUrl).toString();
return { id, fullName, baseURL, token };
}
}
async function uploadAssetsAsync(
assetMap: WorkerAssets.AssetMap,
uploads: Record<string, string>
deployParams: DeployInProgressParams
): Promise<void> {
if (typeof uploads !== 'object' || !uploads) {
return;
}
// TODO(@kitten): Batch and upload multiple files in parallel
const uploadParams: UploadParams[] = [];
const assetPath = projectDist.type === 'server' ? projectDist.clientPath : projectDist.path;
if (!assetPath) {
@@ -170,10 +203,9 @@ export default class WorkerDeploy extends EasCommand {
}
for await (const asset of WorkerAssets.listAssetMapFilesAsync(assetPath, assetMap)) {
const uploadURL = uploads[asset.normalizedPath];
if (uploadURL) {
uploadParams.push({ url: uploadURL, filePath: asset.path });
}
const uploadURL = new URL(`/asset/${asset.sha512}`, deployParams.baseURL);
uploadURL.searchParams.set('token', deployParams.token);
uploadParams.push({ url: uploadURL.toString(), filePath: asset.path });
}
const progress = {
@@ -214,7 +246,7 @@ export default class WorkerDeploy extends EasCommand {
let assetMap: WorkerAssets.AssetMap;
let tarPath: string;
let deployResult: any;
let deployResult: DeployInProgressParams;
let progress = ora('Preparing project').start();
try {
@@ -253,7 +285,8 @@ export default class WorkerDeploy extends EasCommand {
throw error;
}
await uploadAssetsAsync(assetMap, deployResult.uploads);
await uploadAssetsAsync(assetMap, deployResult);
await finalizeDeployAsync(deployResult);
let deploymentAlias: null | Awaited<ReturnType<typeof assignWorkerDeploymentAliasAsync>> = null;
if (flags.aliasName) {
+76 -17
View File
@@ -374,6 +374,7 @@ export type AccountEnvironmentSecretsArgs = {
* data and settings. Actors may own and be members of accounts.
*/
export type AccountEnvironmentVariablesArgs = {
environment?: InputMaybe<EnvironmentVariableEnvironment>;
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
};
@@ -383,6 +384,7 @@ export type AccountEnvironmentVariablesArgs = {
* data and settings. Actors may own and be members of accounts.
*/
export type AccountEnvironmentVariablesIncludingSensitiveArgs = {
environment?: InputMaybe<EnvironmentVariableEnvironment>;
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
};
@@ -3298,9 +3300,11 @@ export type CreateEnvironmentSecretInput = {
};
export type CreateEnvironmentVariableInput = {
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
name: Scalars['String']['input'];
overwrite?: InputMaybe<Scalars['Boolean']['input']>;
type?: InputMaybe<EnvironmentSecretType>;
value: Scalars['String']['input'];
visibility: EnvironmentVariableVisibility;
};
@@ -3363,8 +3367,10 @@ export type CreateServerlessFunctionUploadUrlResult = {
};
export type CreateSharedEnvironmentVariableInput = {
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
name: Scalars['String']['input'];
overwrite?: InputMaybe<Scalars['Boolean']['input']>;
type?: InputMaybe<EnvironmentSecretType>;
value: Scalars['String']['input'];
visibility: EnvironmentVariableVisibility;
};
@@ -3924,14 +3930,28 @@ export type EnvironmentVariable = {
apps: Array<App>;
createdAt: Scalars['DateTime']['output'];
environment?: Maybe<EnvironmentVariableEnvironment>;
environments?: Maybe<Array<EnvironmentVariableEnvironment>>;
id: Scalars['ID']['output'];
linkedEnvironments?: Maybe<Array<EnvironmentVariableEnvironment>>;
name: Scalars['String']['output'];
scope: EnvironmentVariableScope;
type: EnvironmentSecretType;
updatedAt: Scalars['DateTime']['output'];
value?: Maybe<Scalars['String']['output']>;
visibility?: Maybe<EnvironmentVariableVisibility>;
};
export type EnvironmentVariableLinkedEnvironmentsArgs = {
appFullName?: InputMaybe<Scalars['String']['input']>;
appId?: InputMaybe<Scalars['String']['input']>;
};
export type EnvironmentVariableValueArgs = {
includeFileContent?: InputMaybe<Scalars['Boolean']['input']>;
};
export enum EnvironmentVariableEnvironment {
Development = 'DEVELOPMENT',
Preview = 'PREVIEW',
@@ -3956,6 +3976,8 @@ export type EnvironmentVariableMutation = {
linkSharedEnvironmentVariable: EnvironmentVariable;
/** Unlink shared environment variable */
unlinkSharedEnvironmentVariable: EnvironmentVariable;
/** Update an environment variable */
updateEnvironmentVariable: EnvironmentVariable;
};
@@ -3995,17 +4017,22 @@ export type EnvironmentVariableMutationLinkBulkSharedEnvironmentVariablesArgs =
export type EnvironmentVariableMutationLinkSharedEnvironmentVariableArgs = {
appId: Scalars['ID']['input'];
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
};
export type EnvironmentVariableMutationUnlinkSharedEnvironmentVariableArgs = {
appId: Scalars['ID']['input'];
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
};
export type EnvironmentVariableMutationUpdateEnvironmentVariableArgs = {
environmentVariableData: UpdateEnvironmentVariableInput;
};
export enum EnvironmentVariableScope {
Project = 'PROJECT',
Shared = 'SHARED'
@@ -4022,15 +4049,29 @@ export type EnvironmentVariableWithSecret = {
apps: Array<App>;
createdAt: Scalars['DateTime']['output'];
environment?: Maybe<EnvironmentVariableEnvironment>;
environments?: Maybe<Array<EnvironmentVariableEnvironment>>;
id: Scalars['ID']['output'];
linkedEnvironments?: Maybe<Array<EnvironmentVariableEnvironment>>;
name: Scalars['String']['output'];
scope: EnvironmentVariableScope;
sensitive: Scalars['Boolean']['output'];
type: EnvironmentSecretType;
updatedAt: Scalars['DateTime']['output'];
value?: Maybe<Scalars['String']['output']>;
visibility: EnvironmentVariableVisibility;
};
export type EnvironmentVariableWithSecretLinkedEnvironmentsArgs = {
appFullName?: InputMaybe<Scalars['String']['input']>;
appId?: InputMaybe<Scalars['String']['input']>;
};
export type EnvironmentVariableWithSecretValueArgs = {
includeFileContent?: InputMaybe<Scalars['Boolean']['input']>;
};
export type EstimatedOverageAndCost = {
__typename?: 'EstimatedOverageAndCost';
id: Scalars['ID']['output'];
@@ -4989,7 +5030,7 @@ export type LineDataset = {
export type LinkSharedEnvironmentVariableInput = {
appId: Scalars['ID']['input'];
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
};
@@ -6602,6 +6643,15 @@ export type UpdateDeploymentsConnection = {
pageInfo: PageInfo;
};
export type UpdateEnvironmentVariableInput = {
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
id: Scalars['ID']['input'];
name?: InputMaybe<Scalars['String']['input']>;
type?: InputMaybe<EnvironmentSecretType>;
value?: InputMaybe<Scalars['String']['input']>;
visibility?: InputMaybe<EnvironmentVariableVisibility>;
};
export type UpdateGitHubBuildTriggerInput = {
autoSubmit: Scalars['Boolean']['input'];
buildProfile: Scalars['String']['input'];
@@ -8381,21 +8431,21 @@ export type DeleteEnvironmentSecretMutation = { __typename?: 'RootMutation', env
export type LinkSharedEnvironmentVariableMutationVariables = Exact<{
appId: Scalars['ID']['input'];
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
}>;
export type LinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', linkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } };
export type LinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', linkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } };
export type UnlinkSharedEnvironmentVariableMutationVariables = Exact<{
appId: Scalars['ID']['input'];
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
environmentVariableId: Scalars['ID']['input'];
}>;
export type UnlinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', unlinkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } };
export type UnlinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', unlinkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } };
export type CreateEnvironmentVariableForAccountMutationVariables = Exact<{
input: CreateSharedEnvironmentVariableInput;
@@ -8403,7 +8453,7 @@ export type CreateEnvironmentVariableForAccountMutationVariables = Exact<{
}>;
export type CreateEnvironmentVariableForAccountMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForAccount: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } };
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 } } };
export type CreateEnvironmentVariableForAppMutationVariables = Exact<{
input: CreateEnvironmentVariableInput;
@@ -8411,7 +8461,14 @@ export type CreateEnvironmentVariableForAppMutationVariables = Exact<{
}>;
export type CreateEnvironmentVariableForAppMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForApp: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } };
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 } } };
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 } } };
export type DeleteEnvironmentVariableMutationVariables = Exact<{
id: Scalars['ID']['input'];
@@ -8661,36 +8718,38 @@ export type EnvironmentSecretsByAppIdQuery = { __typename?: 'RootQuery', app: {
export type EnvironmentVariablesIncludingSensitiveByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
}>;
export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: 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<EnvironmentVariableEnvironment> | null }> } } };
export type EnvironmentVariablesByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment: EnvironmentVariableEnvironment;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
}>;
export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }> } } };
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 }> } } };
export type EnvironmentVariablesSharedQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
}>;
export type EnvironmentVariablesSharedQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | 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<EnvironmentVariableEnvironment> | null, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }> } } } };
export type EnvironmentVariablesSharedWithSensitiveQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
environment?: InputMaybe<EnvironmentVariableEnvironment>;
}>;
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 }> } } } };
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 }> } } } };
export type GoogleServiceAccountKeyByIdQueryVariables = Exact<{
ascApiKeyId: Scalars['ID']['input'];
@@ -8809,7 +8868,7 @@ export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, s
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, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null };
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 };
export type RuntimeFragment = { __typename?: 'Runtime', id: string, version: string };
@@ -8,18 +8,22 @@ import {
CreateEnvironmentVariableForAccountMutation,
CreateEnvironmentVariableForAppMutation,
DeleteEnvironmentVariableMutation,
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableVisibility,
LinkSharedEnvironmentVariableMutation,
UnlinkSharedEnvironmentVariableMutation,
UpdateEnvironmentVariableMutation,
} from '../generated';
import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
type UpdateVariableArgs = {
value?: string;
type CreateVariableArgs = {
value: string;
name: string;
overwrite: true;
visibility?: EnvironmentVariableVisibility;
visibility: EnvironmentVariableVisibility;
environments: EnvironmentVariableEnvironment[];
type: EnvironmentSecretType;
};
export type EnvironmentVariablePushInput = {
@@ -35,7 +39,7 @@ export const EnvironmentVariableMutation = {
graphqlClient: ExpoGraphqlClient,
environmentVariableId: string,
appId: string,
environment: string
environment?: EnvironmentVariableEnvironment
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -43,7 +47,7 @@ export const EnvironmentVariableMutation = {
gql`
mutation LinkSharedEnvironmentVariable(
$appId: ID!
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
$environmentVariableId: ID!
) {
environmentVariable {
@@ -70,7 +74,7 @@ export const EnvironmentVariableMutation = {
graphqlClient: ExpoGraphqlClient,
environmentVariableId: string,
appId: string,
environment: string
environment?: EnvironmentVariableEnvironment
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -78,7 +82,7 @@ export const EnvironmentVariableMutation = {
gql`
mutation UnlinkSharedEnvironmentVariable(
$appId: ID!
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
$environmentVariableId: ID!
) {
environmentVariable {
@@ -103,14 +107,7 @@ export const EnvironmentVariableMutation = {
},
async createSharedVariableAsync(
graphqlClient: ExpoGraphqlClient,
input:
| {
name: string;
value: string;
visibility: EnvironmentVariableVisibility;
overwrite?: boolean;
}
| UpdateVariableArgs,
input: CreateVariableArgs,
accountId: string
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
@@ -142,15 +139,7 @@ export const EnvironmentVariableMutation = {
},
async createForAppAsync(
graphqlClient: ExpoGraphqlClient,
input:
| {
name: string;
value?: string;
environment: string;
visibility: EnvironmentVariableVisibility;
overwrite?: boolean;
}
| (UpdateVariableArgs & { environment: string }),
input: CreateVariableArgs,
appId: string
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
@@ -177,6 +166,31 @@ export const EnvironmentVariableMutation = {
return data.environmentVariable.createEnvironmentVariableForApp;
},
async updateAsync(
graphqlClient: ExpoGraphqlClient,
input: Partial<CreateVariableArgs> & { id: string }
): Promise<EnvironmentVariableFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateEnvironmentVariableMutation>(
gql`
mutation UpdateEnvironmentVariable($input: UpdateEnvironmentVariableInput!) {
environmentVariable {
updateEnvironmentVariable(environmentVariableData: $input) {
id
...EnvironmentVariableFragment
}
}
}
${print(EnvironmentVariableFragmentNode)}
`,
{ input }
)
.toPromise()
);
return data.environmentVariable.updateEnvironmentVariable;
},
async deleteAsync(graphqlClient: ExpoGraphqlClient, id: string): Promise<{ id: string }> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -4,6 +4,7 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariablesByAppIdQuery,
EnvironmentVariablesSharedQuery,
@@ -11,6 +12,10 @@ import {
} from '../generated';
import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
type EnvironmentVariableWithLinkedEnvironments = EnvironmentVariableFragment & {
linkedEnvironments?: EnvironmentVariableEnvironment[] | null;
};
export const EnvironmentVariablesQuery = {
async byAppIdWithSensitiveAsync(
graphqlClient: ExpoGraphqlClient,
@@ -20,7 +25,7 @@ export const EnvironmentVariablesQuery = {
filterNames,
}: {
appId: string;
environment: string;
environment?: EnvironmentVariableEnvironment;
filterNames?: string[];
}
): Promise<EnvironmentVariableFragment[]> {
@@ -31,7 +36,7 @@ export const EnvironmentVariablesQuery = {
query EnvironmentVariablesIncludingSensitiveByAppId(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
@@ -43,6 +48,7 @@ export const EnvironmentVariablesQuery = {
id
name
value
environments
}
}
}
@@ -64,10 +70,10 @@ export const EnvironmentVariablesQuery = {
filterNames,
}: {
appId: string;
environment: string;
environment?: EnvironmentVariableEnvironment;
filterNames?: string[];
}
): Promise<EnvironmentVariableFragment[]> {
): Promise<EnvironmentVariableWithLinkedEnvironments[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<EnvironmentVariablesByAppIdQuery>(
@@ -75,13 +81,14 @@ export const EnvironmentVariablesQuery = {
query EnvironmentVariablesByAppId(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment!
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
id
environmentVariables(filterNames: $filterNames, environment: $environment) {
id
linkedEnvironments(appId: $appId)
...EnvironmentVariableFragment
}
}
@@ -99,20 +106,29 @@ export const EnvironmentVariablesQuery = {
},
async sharedAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, filterNames }: { appId: string; filterNames?: string[] }
): Promise<EnvironmentVariableFragment[]> {
{
appId,
filterNames,
environment,
}: { appId: string; filterNames?: string[]; environment?: EnvironmentVariableEnvironment }
): Promise<EnvironmentVariableWithLinkedEnvironments[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<EnvironmentVariablesSharedQuery, EnvironmentVariablesSharedQueryVariables>(
gql`
query EnvironmentVariablesShared($appId: String!, $filterNames: [String!]) {
query EnvironmentVariablesShared(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
id
ownerAccount {
id
environmentVariables(filterNames: $filterNames) {
environmentVariables(filterNames: $filterNames, environment: $environment) {
id
linkedEnvironments(appId: $appId)
...EnvironmentVariableFragment
}
}
@@ -121,7 +137,7 @@ export const EnvironmentVariablesQuery = {
}
${print(EnvironmentVariableFragmentNode)}
`,
{ appId, filterNames },
{ appId, filterNames, environment },
{ additionalTypenames: ['EnvironmentVariable'] }
)
.toPromise()
@@ -131,7 +147,11 @@ export const EnvironmentVariablesQuery = {
},
async sharedWithSensitiveAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, filterNames }: { appId: string; filterNames?: string[] }
{
appId,
filterNames,
environment,
}: { appId: string; filterNames?: string[]; environment?: EnvironmentVariableEnvironment }
): Promise<EnvironmentVariableFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
@@ -140,23 +160,28 @@ export const EnvironmentVariablesQuery = {
query EnvironmentVariablesSharedWithSensitive(
$appId: String!
$filterNames: [String!]
$environment: EnvironmentVariableEnvironment
) {
app {
byId(appId: $appId) {
id
ownerAccount {
id
environmentVariablesIncludingSensitive(filterNames: $filterNames) {
environmentVariablesIncludingSensitive(
filterNames: $filterNames
environment: $environment
) {
id
name
value
environments
}
}
}
}
}
`,
{ appId, filterNames },
{ appId, filterNames, environment },
{ additionalTypenames: ['EnvironmentVariableWithSecret'] }
)
.toPromise()
@@ -5,8 +5,9 @@ export const EnvironmentVariableFragmentNode = gql`
id
name
value
environment
environments
createdAt
updatedAt
scope
visibility
}
+5
View File
@@ -69,6 +69,11 @@ export async function republishAsync({
'All updates must either be roll back to embedded updates or not'
);
assert(
!updatesToPublish.some(u => !!u.rolloutControlUpdate),
'Cannot republish an update that is being rolled-out. Either complete the update rollout and then republish or publish a new rollout update.'
);
const { runtimeVersion } = arbitraryUpdate;
// If codesigning was created for the original update, we need to add it to the republish.
@@ -1,14 +0,0 @@
import dateFormat from 'dateformat';
import formatFields from './formatFields';
import { EnvironmentVariableFragment } from '../graphql/generated';
export function formatVariable(variable: EnvironmentVariableFragment): string {
return formatFields([
{ label: 'ID', value: variable.id },
{ label: 'Name', value: variable.name },
{ label: 'Value', value: variable.value ?? '(secret)' },
{ label: 'Scope', value: variable.scope },
{ label: 'Created at', value: dateFormat(variable.createdAt, 'mmm dd HH:MM:ss') },
]);
}
+70 -10
View File
@@ -1,21 +1,77 @@
import chalk from 'chalk';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import capitalize from './expodash/capitalize';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableVisibility,
} from '../graphql/generated';
import { promptAsync, selectAsync } from '../prompts';
export async function promptVariableEnvironmentAsync(
nonInteractive: boolean
): Promise<EnvironmentVariableEnvironment> {
export async function promptVariableVisibilityAsync(
nonInteractive: boolean,
selectedVisibility?: EnvironmentVariableVisibility | null
): Promise<EnvironmentVariableVisibility> {
if (nonInteractive) {
throw new Error(
'The `--visibility` flag must be set when running in `--non-interactive` mode.'
);
}
return await selectAsync(
'Select visibility:',
Object.values(EnvironmentVariableVisibility).map(visibility => ({
title: capitalize(visibility),
value: visibility,
selected: visibility === selectedVisibility,
}))
);
}
type EnvironmentPromptArgs = {
nonInteractive: boolean;
selectedEnvironments?: EnvironmentVariableEnvironment[];
availableEnvironments?: EnvironmentVariableEnvironment[];
};
export function promptVariableEnvironmentAsync(
input: EnvironmentPromptArgs & { multiple: true }
): Promise<EnvironmentVariableEnvironment[]>;
export function promptVariableEnvironmentAsync(
input: EnvironmentPromptArgs & { multiple?: false }
): Promise<EnvironmentVariableEnvironment>;
export async function promptVariableEnvironmentAsync({
nonInteractive,
selectedEnvironments,
multiple = false,
availableEnvironments,
}: EnvironmentPromptArgs & { multiple?: boolean }): Promise<
EnvironmentVariableEnvironment[] | EnvironmentVariableEnvironment
> {
if (nonInteractive) {
throw new Error(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
}
return await selectAsync('Select environment:', [
{ title: 'development', value: EnvironmentVariableEnvironment.Development },
{ title: 'preview', value: EnvironmentVariableEnvironment.Preview },
{ title: 'production', value: EnvironmentVariableEnvironment.Production },
]);
if (!multiple) {
return await selectAsync(
'Select environment:',
(availableEnvironments ?? Object.values(EnvironmentVariableEnvironment)).map(environment => ({
title: environment,
value: environment,
}))
);
}
const { environments } = await promptAsync({
message: 'Select environment:',
name: 'environments',
type: 'multiselect',
choices: Object.values(EnvironmentVariableEnvironment).map(environment => ({
title: environment,
value: environment,
selected: selectedEnvironments?.includes(environment),
})),
});
return environments;
}
export async function promptVariableValueAsync({
@@ -64,7 +120,10 @@ export async function promptVariableValueAsync({
return variableValue;
}
export async function promptVariableNameAsync(nonInteractive: boolean): Promise<string> {
export async function promptVariableNameAsync(
nonInteractive: boolean,
initialValue?: string
): Promise<string> {
const validationMessage = 'Variable name may not be empty.';
if (nonInteractive) {
throw new Error(validationMessage);
@@ -74,6 +133,7 @@ export async function promptVariableNameAsync(nonInteractive: boolean): Promise<
type: 'text',
name: 'name',
message: `Variable name:`,
initial: initialValue,
validate: value => {
if (!value) {
return validationMessage;
@@ -0,0 +1,35 @@
import dateFormat from 'dateformat';
import formatFields from './formatFields';
import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
} from '../graphql/generated';
export function formatVariableName(variable: EnvironmentVariableFragment): string {
const name = variable.name;
const scope = variable.scope === EnvironmentVariableScope.Project ? 'project' : 'shared';
const environments = variable.environments?.join(', ') ?? '';
const updatedAt = variable.updatedAt ? new Date(variable.updatedAt).toLocaleString() : '';
return `${name} | ${scope} | ${environments} | Updated at: ${updatedAt}`;
}
export async function performForEnvironmentsAsync(
environments: EnvironmentVariableEnvironment[] | null,
fun: (environment: EnvironmentVariableEnvironment | undefined) => Promise<any>
): Promise<any[]> {
const selectedEnvironments = environments ?? [undefined];
return await Promise.all(selectedEnvironments.map(env => fun(env)));
}
export function formatVariable(variable: EnvironmentVariableFragment): string {
return formatFields([
{ label: 'ID', value: variable.id },
{ label: 'Name', value: variable.name },
{ label: 'Value', value: variable.value ?? '(secret)' },
{ label: 'Scope', value: variable.scope },
{ label: 'Created at', value: dateFormat(variable.createdAt, 'mmm dd HH:MM:ss') },
{ label: 'Updated at', value: dateFormat(variable.updatedAt, 'mmm dd HH:MM:ss') },
]);
}
+8 -3
View File
@@ -144,18 +144,23 @@ async function* listWorkerFilesAsync(workerPath: string): AsyncGenerator<WorkerF
}
}
interface AssetFileEntry {
normalizedPath: string;
sha512: string;
path: string;
}
/** Reads files of an asset maps and enumerates normalized paths and data */
async function* listAssetMapFilesAsync(
assetPath: string,
assetMap: AssetMap
): AsyncGenerator<WorkerFileEntry> {
): AsyncGenerator<AssetFileEntry> {
for (const normalizedPath in assetMap) {
const filePath = path.resolve(assetPath, normalizedPath.split('/').join(path.sep));
const data = await fs.promises.readFile(filePath);
yield {
normalizedPath,
path: filePath,
data,
sha512: assetMap[normalizedPath],
};
}
}
+22
View File
@@ -139,6 +139,28 @@ export async function uploadAsync(params: UploadParams): Promise<UploadResult> {
);
}
export async function callUploadApiAsync(url: string | URL, init?: RequestInit): Promise<unknown> {
return await promiseRetry(async retry => {
let response: Response;
try {
response = await fetch(url, {
...init,
agent: getAgent(),
});
} catch (error) {
return retry(error);
}
if (response.status >= 500 && response.status <= 599) {
retry(new Error(`Deployment failed: ${response.statusText}`));
}
try {
return await response.json();
} catch (error) {
retry(error);
}
});
}
export interface UploadPending {
params: UploadParams;
}
+5 -4
View File
@@ -5,6 +5,7 @@ import type {
WorkerDeploymentFragment,
} from '../../graphql/generated';
import formatFields, { type FormatFieldsItem } from '../../utils/formatFields';
import { link } from '../../log';
export const EXPO_BASE_DOMAIN = process.env.EXPO_STAGING ? 'staging.expo' : 'expo';
@@ -29,18 +30,18 @@ type WorkerDeploymentData = {
export function formatWorkerDeploymentTable(data: WorkerDeploymentData): string {
const fields: FormatFieldsItem[] = [
{ label: 'Dashboard', value: getDashboardUrl(data.projectId) },
{ label: 'Deployment URL', value: data.deployment.url },
{ label: 'Dashboard', value: link(getDashboardUrl(data.projectId)) },
{ label: 'Deployment URL', value: link(data.deployment.url) },
];
if (data.aliases?.length) {
const alias = data.aliases.filter(Boolean)[0];
if (alias) {
fields.push({ label: 'Alias URL', value: alias.url });
fields.push({ label: 'Alias URL', value: link(alias.url) });
}
}
if (data.production) {
fields.push({ label: 'Production URL', value: data.production.url });
fields.push({ label: 'Production URL', value: link(data.production.url) });
}
const lastUrlField = fields[fields.length - 1];
+2 -1
View File
@@ -378,6 +378,7 @@
"sdk-51",
"sdk-50",
"sdk-49",
"macos-sonoma-14.6-xcode-16.0",
"macos-sonoma-14.5-xcode-15.4",
"macos-sonoma-14.4-xcode-15.3",
"macos-ventura-13.6-xcode-15.2",
@@ -386,7 +387,7 @@
],
"markdownEnumDescriptions": [
"When using this option the build image is selected automatically based on the project configuration, detected Expo SDK and React Native versions.",
"The latest iOS image currently available. It is resolved to `macos-sonoma-14.5-xcode-15.4`. The `latest` to image mapping will be updated as new images are released.",
"The latest iOS image currently available. It is resolved to `macos-sonoma-14.6-xcode-16.0`. The `latest` to image mapping will be updated as new images are released.",
"The recommended image for SDK 51 builds",
"The recommended image for SDK 50 builds",
"The recommended image for SDK 49 builds"