Compare commits

..
Author SHA1 Message Date
Brent Vatne 64d985fcda v3.1.0 2022-12-16 15:54:02 -08:00
Jon Samp bd2bb4dcf6 [eas-cli] Add --channel flag to eas update command (#1567)
* Add --channel flag to eas update command

* use existing method

* adds undefined to return type

* move to getBranchNameFromChannelNameAsync.ts

* renames the function to singular "update"
2022-12-15 10:56:30 -06:00
Wojciech Kozyra 17db6c4cf1 [eas-cli] Add build:resign command (#1575) 2022-12-15 14:23:10 +01:00
Jon SampandDominik Sokal 5203096aca [eas-cli] Adds channels to build profiles on eas update:configure (#1570)
* Adds eas.json configuration during eas update configure

* updates next steps message

* renames eas updates to eas update

* self feedback

* update CHANGELOG.md

* Update packages/eas-cli/src/commands/update/configure.ts

Co-authored-by: Dominik Sokal <[email protected]>

* Update packages/eas-cli/src/commands/update/configure.ts

Co-authored-by: Dominik Sokal <[email protected]>

* code style feedback incorp

* use EasJsonAccessor

* Update packages/eas-cli/src/update/configure.ts

Co-authored-by: Dominik Sokal <[email protected]>

* lint

Co-authored-by: Dominik Sokal <[email protected]>
2022-12-12 10:59:41 -06:00
Expo CI a53e1f916f update CHANGELOG.md 2022-12-07 10:15:44 +00:00
Szymon Dziedzic 09a4b3d387 v3.0.0 2022-12-07 11:01:22 +01:00
Szymon Dziedzic 24e7de5bfb [eas-cli] Fix eas cli node:assert import error by using imports from assert instead of node:assert (#1569)
* [eas-cli] use import from 'assert' instead from 'node:assert'

* update CHANGELOG.md
2022-12-07 10:57:45 +01:00
Szymon Dziedzic 613fb5b6ea [eas-json] Make not recognized image error handler run only if image is present under valid key with invalid value (#1565)
* [eas-json] Make not recognized image error handler fire only if image is present under valid key with invalid value

* revert changes to error message

* update CHANGELOG.md
2022-12-06 14:08:03 +01:00
Cedric van Putten 4e117075fa refactor(update): remove republish from update command (#1535)
* fix(update): add code signing support to republish and block different branches

* refactor(update): move codesigning info into update fragment

* update CHANGELOG.md

* fix(update): add code signing support to republish and block different branches

* refactor(update): mark group and republish flags as deprecated

* refactor(update): drop republish and group flags

* refactor(update): drop republish flag from command

* refactor(update): remove last republish code from update command

* refactor(update): either use --group or --branch in deprecation warning

* refactor(update): drop old runtime version from update command

* refactor(update): move asset variables closer to populated area

* fix(update): fix republish typo in deprecation warning

* fix(update): correct a merge conflict error related to publish query changes
2022-12-05 13:25:52 +01:00
Cedric van Putten 33f4ae7a05 [eas-cli] Split republish command from update command (#1533)
* refactor(update): split out republish to separate command

* fix(update): add code signing support to republish and block different branches

* test(update): add missing republish command tests

* test(update): add test case without codesigning

* refactor(update): move codesigning info into update fragment

* refactor(update): make branch and group exclusive and simplify code

* update CHANGELOG.md
2022-12-05 04:08:56 -08:00
Expo CI caba28901d update CHANGELOG.md 2022-12-05 12:03:51 +00:00
Szymon Dziedzic 77e02f5759 v2.9.0 2022-12-05 12:55:30 +01:00
Szymon Dziedzic fa2717aada [ENG-6238][eas-cli] prompt developer to download the simulator app after build is finished when in interactive mode (#1554)
* [eas-cli] prompt developer to download the simulator app when in interactive mode

* check if build status is finished

* improvements after code review

* fix iOS simulator select prompt

* fix typo

* improve prompt message

* update CHANGELOG.md
2022-12-05 12:54:08 +01:00
Szymon Dziedzic 322f797cbe [ENG-7007][eas-cli] change Android start intent to MAIN instead of RUN in eas build:run (#1556)
* [eas-cli] change am start intent to MAIN instead of RUN in eas build:run command

* update CHANGELOG.md
2022-12-01 17:35:10 +01:00
Szymon Dziedzic 2a64b099ed [ENG-7006][eas-cli] Indicate in eas build:run when build artifacts has expired (#1547)
* [eas-cli] inidicate in `eas build:run` when build artifacts has expired

* minor changes after code review

* update CHANGELOG.md
2022-12-01 11:25:40 +01:00
Juwan WheatleyandDominik Sokal 81465733e8 [eas-cli] add isGitWorkingTreeDirty to EAS Update publish command (#1550)
* [eas-cli] add isGitWorkingTreeDirty to EAS Update publish command

* update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2022-11-30 15:10:46 -05:00
Szymon Dziedzic 9b22056205 [ENG-6238][eas-cli] add caching for eas build run command (#1542)
* [eas-cli] add caching for `eas build:run`

* cache in tmp directory

* use path from `path` instead of `node:path`

* don't hide eas build:run command

* apply feedback from review

* improvements after code review

* update CHANGELOG.md
2022-11-29 12:12:32 +01:00
Wojciech Kozyra 24841f82e0 [eas-cli] Consistent prompt for application identifier for both platforms (#1543) 2022-11-29 11:38:47 +01:00
Expo CI a8fae470d0 update CHANGELOG.md 2022-11-28 11:25:27 +00:00
Dominik Sokal 72283cdda2 v2.8.0 2022-11-28 12:13:38 +01:00
Dominik Sokal 9874ff26b3 upgrade eas-cli-local-build-plugin to 0.0.120 2022-11-28 12:13:16 +01:00
Dominik Sokal 85d64b7ad3 [ENG-6491] remove internal expo id from device:list (#1541)
* remove internal expo id from device:list

* update CHANGELOG.md
2022-11-28 09:34:36 +01:00
Szymon Dziedzic 2dba2705b7 [ENG-6238][eas-cli] add build:run support for Android (#1485)
* [eas-cli] add build:run support for Android

* add assertions about android executables

* use spawnAsync instead of spawn

* minor refactor

* minor improvements to prompts

* improve download function name

* refactor code after the review

* apply suggestions from the review

* fix commit message formatting

* use progress tracker instead of progress bar

* improve formatting of build choices

* fix typo in function name

* requested improvements

* test using path.join for fast-lob

* fix windows glob issues

* fix glob on windows

* allow users to run ios builds only on apple device

* use path.join instead of interpolation

* address feedback

* auto select android for non mac users

* minor improvements

* improve error messages

* update CHANGELOG.md
2022-11-25 13:57:19 +01:00
Cedric van Putten d1bec6e00a [eas-cli] Only create a channel with branch name when branch is created (#1507)
* fix(update): only create a channel with branch name when branch is created

* refactor(update): rename branchIsCreated to createdBranch

* update CHANGELOG.md
2022-11-23 18:28:08 +01:00
Szymon Dziedzic 597e012ca3 [ENG-6941][eas-json] add better error message for when invalid image is set in the eas.json (#1531)
* [eas-json] add custom error message handler for invalid image validation error

* update CHANGELOG.md
2022-11-21 15:52:30 +01:00
55 changed files with 2960 additions and 1666 deletions

No files matched your search

+48 -1
View File
@@ -8,12 +8,59 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Adds `--channel` flag to `eas update`. ([#1567](https://github.com/expo/eas-cli/pull/1567) by [@jonsamp](https://github.com/jonsamp))
- Add channel configurations to **eas.json** during `eas update:configure`. ([#1570](https://github.com/expo/eas-cli/pull/1570) by [@jonsamp](https://github.com/jonsamp))
- Add `build:resign` command. ([#1575](https://github.com/expo/eas-cli/pull/1575) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
### 🧹 Chores
## [3.0.0](https://github.com/expo/eas-cli/releases/tag/v3.0.0) - 2022-12-07
### 🛠 Breaking changes
- Move update republish to separate command. ([#1533](https://github.com/expo/eas-cli/pull/1533) by [@byCedric](https://github.com/byCedric))
### 🐛 Bug fixes
- Make the not recognized image error handler run only if the image is present under a valid key and has an invalid value. ([#1565](https://github.com/expo/eas-cli/pull/1565) by [@szdziedzic](https://github.com/szdziedzic))
- Fix the `node:assert` import error by using imports from `assert` instead of from `node:assert`. ([#1569](https://github.com/expo/eas-cli/pull/1569) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Remove old republish code from the update command. ([#1535](https://github.com/expo/eas-cli/pull/1535) by [@byCedric](https://github.com/byCedric))
## [2.9.0](https://github.com/expo/eas-cli/releases/tag/v2.9.0) - 2022-12-05
### 🎉 New features
- Add caching for `eas build:run` command. ([#1542](https://github.com/expo/eas-cli/pull/1542) by [@szdziedzic](https://github.com/szdziedzic))
- Add `isGitWorkingTreeDirty` to EAS Update records. ([#1550](https://github.com/expo/eas-cli/pull/1550) by [@FiberJW](https://github.com/FiberJW))
- Prompt developer to download the app after the simulator build is finished. ([#1554](https://github.com/expo/eas-cli/pull/1554) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Ask for the missing application identifier consistently in `eas build:version:sync` command. ([#1543](https://github.com/expo/eas-cli/pull/1543) by [@wkozyra95](https://github.com/wkozyra95))
- Disable selecting builds whose artifacts have expired in the `eas build:run` command. ([#1547](https://github.com/expo/eas-cli/pull/1547) by [@szdziedzic](https://github.com/szdziedzic))
- Change intent with which the `eas build:run` command opens an Android app to `android.intent.action.MAIN`. ([#1556](https://github.com/expo/eas-cli/pull/1556) by [@szdziedzic](https://github.com/szdziedzic))
## [2.8.0](https://github.com/expo/eas-cli/releases/tag/v2.8.0) - 2022-11-28
### 🎉 New features
- Add `eas build:run` support for Android. ([#1485](https://github.com/expo/eas-cli/pull/1485) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Only create a new channel when update branch is new. ([#1507](https://github.com/expo/eas-cli/pull/1507) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Better wording in eas submit. ([#1527](https://github.com/expo/eas-cli/pull/1527) by [@dsokal](https://github.com/dsokal))
- Upload project sources for EAS Build and archives for EAS Submit to GCS. ([#1524](https://github.com/expo/eas-cli/pull/1524) by [@wkozyra95](https://github.com/wkozyra95))
- Add a better error message for when an invalid `image` is set in `eas-json`. ([#1531](https://github.com/expo/eas-cli/pull/1531) by [@szdziedzic](https://github.com/szdziedzic))
- Remove internal expo id from device formatter. ([#1541](https://github.com/expo/eas-cli/pull/1541) by [@dsokal](https://github.com/dsokal))
## [2.7.1](https://github.com/expo/eas-cli/releases/tag/v2.7.1) - 2022-11-16
@@ -295,7 +342,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Add new option to `eas device:create` - allow importing devices from Apple Developer Portal. ([#1236](https://github.com/expo/eas-cli/pull/1236) by [@dsokal](https://github.com/dsokal))
- Add support for attaching messages to builds. ([#1237](https://github.com/expo/eas-cli/pull/1237) by [@dsokal](https://github.com/dsokal))
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
## [0.56.0](https://github.com/expo/eas-cli/releases/tag/v0.56.0) - 2022-07-28
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.7.1"
"version": "3.1.0"
}
File diff suppressed because it is too large. Load diff
+600 -56
View File
@@ -2258,22 +2258,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -2438,22 +2422,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -2742,22 +2710,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -5129,6 +5081,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectArchive",
"description": "",
@@ -5435,6 +5399,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseChannel",
"description": "",
@@ -6623,6 +6599,51 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deploymentNew",
"description": "",
"args": [
{
"name": "channel",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtimeVersion",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "OBJECT",
"name": "DeploymentNew",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deployments",
"description": "Deployments associated with this app",
@@ -12012,7 +12033,20 @@
{
"name": "canRetry",
"description": "",
"args": [],
"args": [
{
"name": "newMode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
@@ -13860,6 +13894,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildMode",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "BUILD",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "RESIGN",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BuildMutation",
@@ -14713,6 +14770,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "BuildResignInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "applicationArchiveSource",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "ProjectArchiveSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildResourceClass",
@@ -14849,6 +14929,12 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNKNOWN",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@@ -16499,6 +16585,224 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentBuildEdge",
"description": "",
"fields": [
{
"name": "cursor",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Build",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentBuildsConnection",
"description": "",
"fields": [
{
"name": "edges",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeploymentBuildEdge",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentNew",
"description": "Represents a Deployment - a set of Builds with the same Runtime Version and Channel",
"fields": [
{
"name": "builds",
"description": "",
"args": [
{
"name": "after",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "before",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "first",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "last",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeploymentBuildsConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "channel",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "UpdateChannel",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtime",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Runtime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DeploymentOptions",
@@ -17580,13 +17884,9 @@
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
@@ -20750,6 +21050,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectArchive",
"description": "",
@@ -21021,6 +21333,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseChannel",
"description": "",
@@ -21033,6 +21357,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resign",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "BuildResignInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scheme",
"description": "",
@@ -21069,6 +21405,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildWorkflow",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updates",
"description": "",
@@ -22479,6 +22827,73 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "PageInfo",
"description": "",
"fields": [
{
"name": "endCursor",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasNextPage",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasPreviousPage",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "startCursor",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "PartialManifest",
@@ -22938,6 +23353,12 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "NONE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "S3",
"description": "",
@@ -23267,6 +23688,26 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "excludedAssets",
"description": "",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "PartialManifestAsset",
"ofType": null
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitCommitHash",
"description": "",
@@ -23279,6 +23720,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isGitWorkingTreeDirty",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "",
@@ -25098,6 +25551,81 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Runtime",
"description": "",
"fields": [
{
"name": "app",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "App",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "firstBuildCreatedAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "version",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SecondFactorBooleanResult",
@@ -27659,6 +28187,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isGitWorkingTreeDirty",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "manifestFragment",
"description": "",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "2.7.1",
"version": "3.1.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -14,7 +14,7 @@
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.97",
"@expo/eas-json": "2.7.1",
"@expo/eas-json": "3.0.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
+5 -4
View File
@@ -15,6 +15,7 @@ import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
SelectPromptEntry,
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
@@ -31,7 +32,7 @@ export async function selectBranchOnAppAsync(
paginatedQueryOptions,
}: {
projectId: string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => SelectPromptEntry;
promptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
@@ -160,7 +161,7 @@ export async function ensureBranchExistsAsync(
appId: string;
branchName: string;
}
): Promise<{ branchId: string }> {
): Promise<{ branchId: string; createdBranch: boolean }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
@@ -168,14 +169,14 @@ export async function ensureBranchExistsAsync(
});
const { id } = updateBranch;
return { branchId: id };
return { branchId: id, createdBranch: false };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id };
return { branchId: newUpdateBranch.id, createdBranch: true };
} else {
throw error;
}
+1 -1
View File
@@ -281,7 +281,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
);
}
type MaybeBuildFragment = BuildFragment | null;
export type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient,
@@ -1,5 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { CredentialsContext } from '../../credentials/context';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { IosCredentials, Target } from '../../credentials/ios/types';
@@ -31,6 +33,27 @@ export async function ensureIosCredentialsAsync(
};
}
export async function ensureIosCredentialsForBuildResignAsync(
credentialsCtx: CredentialsContext,
targets: Target[],
buildProfile: BuildProfile<Platform.IOS>
): Promise<CredentialsResult<IosCredentials>> {
const provider = new IosCredentialsProvider(credentialsCtx, {
app: await getAppFromContextAsync(credentialsCtx),
targets,
distribution: 'internal',
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
});
const { credentialsSource } = buildProfile;
logCredentialsSource(credentialsSource, Platform.IOS);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
};
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator;
}
+1 -1
View File
@@ -26,7 +26,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
};
}
function transformIosSecrets(secrets: {
export function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
}): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
@@ -1,8 +1,10 @@
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import path from 'path';
import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -79,6 +81,22 @@ export async function prepareJobAsync(
return sanitizeJob(job);
}
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
for (const targetName of Object.keys(credentials ?? {})) {
buildCredentials.push({
targetName,
provisioningProfileBase64: nullthrows(credentials?.[targetName].provisioningProfile),
distributionCertificate: {
dataBase64: nullthrows(credentials?.[targetName].distributionCertificate.certificateP12),
password: nullthrows(credentials?.[targetName].distributionCertificate.certificatePassword),
},
});
}
return { buildCredentials };
}
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.119';
const PLUGIN_PACKAGE_VERSION = '0.0.120';
export interface LocalBuildOptions {
enable: boolean;
+88 -10
View File
@@ -2,13 +2,16 @@ import chalk from 'chalk';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { BuildFilter, BuildFragment } from '../graphql/generated';
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { appPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { fromNow } from '../utils/date';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
export const BUILDS_LIMIT = 50;
@@ -55,18 +58,87 @@ export async function listAndRenderBuildsOnAppAsync(
}
}
export async function listAndSelectBuildOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
title,
filter,
paginatedQueryOptions,
}: {
projectId: string;
title: string;
filter?: BuildFilter;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<BuildFragment | void> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a build in non-interactive mode.');
} else {
return await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit,
offset,
filter,
}),
promptOptions: {
title,
getIdentifierForQueryItem: build => build.id,
createDisplayTextForSelectionPromptListItem: formatBuildChoiceTitleAndDescription,
},
});
}
}
function formatBuildChoiceValue(value: string | undefined | null): string {
return value ? chalk.bold(value) : chalk.dim('Unknown');
}
function formatBuildChoiceTitleAndDescription(build: BuildFragment): {
title: string;
description: string;
} {
const splitCommitMessage = build.gitCommitMessage?.split('\n');
const formattedCommitData =
build.gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
? `${chalk.dim(build.gitCommitHash.slice(0, 7))} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: 'Unknown';
return {
title: `ID: ${chalk.dim(build.id)} (${chalk.dim(`${fromNow(new Date(build.updatedAt))} ago`)})`,
description: [
`\tVersion: ${formatBuildChoiceValue(build.appVersion)}`,
`\t${
build.platform === AppPlatform.Ios ? 'Build number' : 'Version code'
}: ${formatBuildChoiceValue(build.appBuildVersion)}`,
`\tCommit: ${formattedCommitData}`,
].join('\n'),
};
}
export async function listAndSelectBuildsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
selectedPlatform: AppPlatform,
{
projectId,
projectDisplayName,
filter,
queryOptions,
selectPromptDisabledFunction,
warningMessage,
}: {
projectId: string;
projectDisplayName: string;
filter?: BuildFilter;
queryOptions: PaginatedQueryOptions;
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
warningMessage?: string;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
@@ -82,14 +154,20 @@ export async function listAndSelectBuildsOnAppAsync(
const { selectedSimulatorBuild } = await promptAsync({
type: 'select',
message: `Select simulator build to run for ${projectDisplayName} app`,
message: `Select ${selectedPlatform === AppPlatform.Ios ? 'iOS' : 'Android'} ${
selectedPlatform === AppPlatform.Ios ? 'simulator' : 'emulator'
} build to run for ${projectDisplayName} app`,
name: 'selectedSimulatorBuild',
choices: builds.map(build => ({
title: `id: ${build.id}, platform: ${appPlatformDisplayNames[build.platform]}, version: ${
build.appVersion
}, build number: ${build.appBuildVersion}`,
value: build,
})),
choices: builds.map(build => {
const buildChoice = formatBuildChoiceTitleAndDescription(build);
return {
title: buildChoice.title,
description: buildChoice.description,
value: build,
disabled: selectPromptDisabledFunction?.(build),
};
}),
warn: warningMessage,
});
return selectedSimulatorBuild;
}
@@ -7,6 +7,7 @@ import {
EasJsonUtils,
SubmitProfile,
} from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -37,6 +38,9 @@ import {
validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettings,
} from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts';
import { runAsync } from '../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
import { createSubmissionContextAsync } from '../submit/context';
import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
@@ -46,11 +50,13 @@ import {
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { BuildRequestSender, waitForBuildEndAsync } from './build';
import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './build';
import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
@@ -242,6 +248,9 @@ export async function runBuildAndSubmitAsync(
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
printJsonOnlyOutput(builds);
@@ -409,3 +418,35 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
process.exit(1);
}
}
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
assert(build.artifacts?.applicationArchiveUrl);
const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
build.platform
);
await runAsync(buildPath, build.platform);
}
async function maybeDownloadAndRunSimulatorBuildsAsync(
builds: MaybeBuildFragment[],
flags: BuildFlags
): Promise<void> {
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
if (simBuilds.length > 0 && !flags.autoSubmit && !flags.nonInteractive) {
for (const simBuild of simBuilds) {
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
Log.newLine();
const confirm = await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
});
if (confirm) {
await downloadAndRunAsync(simBuild);
}
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
export class ChannelNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Channel not found.');
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ export async function selectChannelOnAppAsync(
queryChannelsOnAppAsync(graphqlClient, { appId: projectId, limit, offset }),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: updateChannel => updateChannel.name,
createDisplayTextForSelectionPromptListItem: updateChannel => ({ title: updateChannel.name }),
getIdentifierForQueryItem: updateChannel => updateChannel.id,
},
});
@@ -120,7 +120,7 @@ export default class BranchDelete extends EasCommand {
}
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
promptTitle: 'Which branch would you like to delete?',
paginatedQueryOptions,
}));
+3 -1
View File
@@ -57,7 +57,9 @@ export default class BranchView extends EasCommand {
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: paginatedQueryOptions.json,
+1 -1
View File
@@ -245,7 +245,7 @@ export default class Build extends EasCommand {
}
}
async function handleDeprecatedEasJsonAsync(
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
@@ -0,0 +1,296 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import { handleDeprecatedEasJsonAsync } from '.';
import { waitForBuildEndAsync } from '../../build/build';
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags } from '../../commandUtils/pagination';
import { CredentialsContext } from '../../credentials/context';
import {
BuildFragment,
BuildMode,
BuildStatus,
BuildWorkflow,
DistributionType,
IosJobOverridesInput,
ProjectArchiveSourceType,
StatuspageServiceName,
} from '../../graphql/generated';
import { BuildMutation } from '../../graphql/mutations/BuildMutation';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { requestedPlatformDisplayNames, selectPlatformAsync } from '../../platform';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
interface BuildResignFlags {
json: boolean;
nonInteractive: boolean;
offset?: number;
limit?: number;
platform?: Platform;
profile?: string;
maybeBuildId?: string;
wait: boolean;
}
interface RawBuildResignFlags {
json: boolean;
'non-interactive': boolean;
offset: number | undefined;
limit: number | undefined;
platform: 'android' | 'ios' | undefined;
profile: string | undefined;
wait: boolean;
id: string | undefined;
}
export default class BuildResign extends EasCommand {
static override description = 're-sign a build archive';
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
wait: Flags.boolean({
default: true,
allowNo: true,
description: 'Wait for build(s) to complete.',
}),
id: Flags.string({
description: 'ID of the build to re-sign.',
}),
...EasPaginatedQueryFlags,
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(BuildResign);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = this.sanitizeFlags(rawFlags as RawBuildResignFlags);
const { limit, offset, nonInteractive } = flags;
const {
loggedIn: { actor, graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
analytics,
} = await this.getContextAsync(BuildResign, {
nonInteractive: flags.nonInteractive,
});
const maybeBuild = flags.maybeBuildId
? await this.maybeGetBuildAsync(graphqlClient, flags.maybeBuildId)
: undefined;
const platform =
(maybeBuild?.platform.toLowerCase() as Platform | undefined) ??
(await selectPlatformAsync(flags.platform));
if (platform === Platform.ANDROID) {
throw new Error('Re-signing archives is not supported on Android yet.');
}
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
flags.profile ?? 'production'
);
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const build = await this.ensureBuildSelectedAsync(
{ graphqlClient, projectId, platform, nonInteractive, limit, offset },
maybeBuild
);
const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
nonInteractive,
projectDir,
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
easJsonCliConfig,
});
if (buildProfile.credentialsSource !== CredentialsSource.LOCAL && !nonInteractive) {
await credentialsCtx.appStore.ensureAuthenticatedAsync();
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir,
nonInteractive,
exp,
},
buildProfile
);
const targets = await resolveTargetsAsync({
projectDir,
exp,
xcodeBuildContext,
env: buildProfile.env,
});
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
credentialsCtx,
targets,
buildProfile
);
assert(build.artifacts?.applicationArchiveUrl, 'Missing application archive.');
const jobOverrides: IosJobOverridesInput = {
mode: BuildMode.Resign,
type: BuildWorkflow.Unknown,
resign: {
applicationArchiveSource: {
type: ProjectArchiveSourceType.Url,
url: build.artifacts?.applicationArchiveUrl,
},
},
secrets: prepareCredentialsToResign(credentialsResult.credentials),
builderEnvironment: { image: 'default' },
};
const newBuild = await BuildMutation.retryIosBuildAsync(graphqlClient, {
buildId: build.id,
jobOverrides,
});
Log.addNewLineIfNone();
printLogsUrls([newBuild]);
Log.newLine();
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput(newBuild);
}
return;
}
const buildResult = await waitForBuildEndAsync(graphqlClient, {
buildIds: [newBuild.id],
accountName: account.name,
});
if (!flags.json) {
printBuildResults(buildResult);
} else {
assert(buildResult[0], 'missing build results');
printJsonOnlyOutput(buildResult[0]);
}
}
sanitizeFlags(flags: RawBuildResignFlags): BuildResignFlags {
const nonInteractive = flags['non-interactive'];
if (nonInteractive && !flags.id) {
throw new Error(
`${chalk.bold('--id')} is required when running with ${chalk.bold(
'--non-interactive'
)} flag.`
);
}
return {
json: flags.json,
nonInteractive,
offset: flags.offset,
limit: flags.limit,
platform: flags.platform as Platform | undefined,
profile: flags.profile,
maybeBuildId: flags.id,
wait: flags.wait,
};
}
async ensureBuildSelectedAsync(
{
graphqlClient,
projectId,
platform,
nonInteractive,
limit,
offset,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: Platform;
nonInteractive: boolean;
limit?: number;
offset?: number;
},
maybeBuild?: BuildFragment
): Promise<BuildFragment> {
if (maybeBuild) {
return maybeBuild;
}
const build = await listAndSelectBuildOnAppAsync(graphqlClient, {
projectId,
title: 'Which build would you like to re-sign with new credentials?',
paginatedQueryOptions: {
limit,
offset: offset ?? 0,
nonInteractive,
json: false,
},
filter: {
distribution: DistributionType.Internal,
platform: toAppPlatform(platform),
status: BuildStatus.Finished,
},
});
if (!build) {
throw new Error('There are no builds that can be re-signed on this project.');
}
return build;
}
async maybeGetBuildAsync(
graphqlClient: ExpoGraphqlClient,
maybeBuildId?: string,
maybePlatform?: Platform
): Promise<BuildFragment | undefined> {
if (maybeBuildId) {
const build = await BuildQuery.byIdAsync(graphqlClient, maybeBuildId);
if (build.distribution !== DistributionType.Internal) {
throw new Error('This is not an internal distribution build.');
}
if (build.status !== BuildStatus.Finished) {
throw new Error('Only builds that finished successfully can be re-signed.');
}
if (maybePlatform && build.platform !== toAppPlatform(maybePlatform)) {
throw new Error(
`Build with ID ${maybeBuildId} was not created for platform ${requestedPlatformDisplayNames[maybePlatform]}.`
);
}
return build;
}
return undefined;
}
}
+63 -22
View File
@@ -1,9 +1,9 @@
import { Errors, Flags } from '@oclif/core';
import { existsSync } from 'fs-extra';
import assert from 'node:assert';
import assert from 'assert';
import { pathExists } from 'fs-extra';
import path from 'path';
import { getLatestBuildAsync, listAndSelectBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -11,13 +11,18 @@ import {
PaginatedQueryOptions,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { RunArchiveFlags, runAsync } from '../../run/run';
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
import { downloadAndExtractAppAsync, extractAppFromLocalArchiveAsync } from '../../utils/download';
import { isRunnableOnSimulatorOrEmulator } from '../../run/utils';
import {
downloadAndMaybeExtractAppAsync,
extractAppFromLocalArchiveAsync,
} from '../../utils/download';
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
interface RawRunFlags {
latest?: boolean;
@@ -37,8 +42,6 @@ interface RunCommandFlags {
}
export default class Run extends EasCommand {
static override hidden = true;
static override description = 'run simulator/emulator builds from eas-cli';
static override flags = {
@@ -51,7 +54,7 @@ export default class Run extends EasCommand {
exclusive: ['latest', 'id', 'path'],
}),
path: Flags.string({
description: 'Path to the simulator/emulator build archive or simulator build app',
description: 'Path to the simulator/emulator build archive or app',
exclusive: ['latest', 'id', 'url'],
}),
id: Flags.string({
@@ -97,13 +100,19 @@ export default class Run extends EasCommand {
const selectedPlatform = await resolvePlatformAsync(platform);
if (platform === 'ios' && process.platform !== 'darwin') {
Errors.error('You can only use an iOS simulator to run apps on macOS devices', {
exit: 1,
});
}
if (
runArchiveFlags.path &&
!(
(runArchiveFlags.path.endsWith('.tar.gz') ||
runArchiveFlags.path.endsWith('.app') ||
runArchiveFlags.path.endsWith('.apk')) &&
existsSync(runArchiveFlags.path)
(await pathExists(runArchiveFlags.path))
)
) {
Errors.error('The path must point to a .tar.gz archive, .apk file, or .app directory', {
@@ -121,6 +130,10 @@ export default class Run extends EasCommand {
}
async function resolvePlatformAsync(platform?: string): Promise<AppPlatform> {
if (process.platform !== 'darwin') {
return AppPlatform.Android;
}
if (platform && Object.values(AppPlatform).includes(platform.toUpperCase() as AppPlatform)) {
return platform.toUpperCase() as AppPlatform;
}
@@ -143,6 +156,9 @@ async function maybeGetBuildAsync(
projectId: string,
paginatedQueryOptions: PaginatedQueryOptions
): Promise<BuildFragment | null> {
const distributionType =
flags.selectedPlatform === AppPlatform.Ios ? DistributionType.Simulator : undefined;
if (flags.runArchiveFlags.id) {
return BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
} else if (
@@ -151,26 +167,25 @@ async function maybeGetBuildAsync(
!flags.runArchiveFlags.url &&
!flags.runArchiveFlags.latest
) {
return await listAndSelectBuildsOnAppAsync(graphqlClient, {
return await listAndSelectBuildsOnAppAsync(graphqlClient, flags.selectedPlatform, {
projectId,
projectDisplayName: await getDisplayNameForProjectIdAsync(graphqlClient, projectId),
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
distribution: distributionType,
status: BuildStatus.Finished,
},
queryOptions: paginatedQueryOptions,
selectPromptDisabledFunction: build => !isRunnableOnSimulatorOrEmulator(build),
warningMessage:
'Artifacts for this build have expired and are no longer available, or this is not a simulator/emulator build.',
});
} else if (flags.runArchiveFlags.latest) {
return await getLatestBuildAsync(graphqlClient, {
projectId,
filter: {
platform: flags.selectedPlatform,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
distribution: distributionType,
status: BuildStatus.Finished,
},
});
@@ -179,6 +194,17 @@ async function maybeGetBuildAsync(
}
}
function getEasBuildRunCachedAppPath(
projectId: string,
buildId: string,
platform: AppPlatform
): string {
return path.join(
getEasBuildRunCacheDirectoryPath(),
`${projectId}_${buildId}.${platform === AppPlatform.Ios ? 'app' : 'apk'}`
);
}
async function getPathToSimulatorBuildAppAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
@@ -186,25 +212,40 @@ async function getPathToSimulatorBuildAppAsync(
queryOptions: PaginatedQueryOptions
): Promise<string> {
const maybeBuild = await maybeGetBuildAsync(graphqlClient, flags, projectId, queryOptions);
const appExtension = flags.selectedPlatform === AppPlatform.Ios ? 'app' : 'apk';
if (maybeBuild) {
const cachedAppPath = getEasBuildRunCachedAppPath(
projectId,
maybeBuild.id,
flags.selectedPlatform
);
if (await pathExists(cachedAppPath)) {
Log.newLine();
Log.log(`Using cached app...`);
return cachedAppPath;
}
if (!maybeBuild.artifacts?.applicationArchiveUrl) {
throw new Error('Build does not have an application archive url');
}
return await downloadAndExtractAppAsync(
return await downloadAndMaybeExtractAppAsync(
maybeBuild.artifacts.applicationArchiveUrl,
appExtension
flags.selectedPlatform,
cachedAppPath
);
}
if (flags.runArchiveFlags.url) {
return await downloadAndExtractAppAsync(flags.runArchiveFlags.url, appExtension);
return await downloadAndMaybeExtractAppAsync(flags.runArchiveFlags.url, flags.selectedPlatform);
}
if (flags.runArchiveFlags.path?.endsWith('.tar.gz')) {
return await extractAppFromLocalArchiveAsync(flags.runArchiveFlags.path!, appExtension);
return await extractAppFromLocalArchiveAsync(
flags.runArchiveFlags.path,
flags.selectedPlatform
);
}
// this should never fail, due to the validation in sanitizeFlagsAsync
@@ -107,7 +107,7 @@ export default class ChannelEdit extends EasCommand {
: await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like ${existingChannel.name} to point at?`,
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
paginatedQueryOptions: {
json,
nonInteractive,
@@ -0,0 +1,237 @@
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import ProjectConfigContextField from '../../../commandUtils/context/ProjectConfigContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { CodeSigningInfo, UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { UpdateQuery } from '../../../graphql/queries/UpdateQuery';
import UpdateRepublish from '../republish';
const projectRoot = '/test-project';
const commandOptions = { root: projectRoot } as any;
const updateStub: UpdateFragment = {
id: 'update-1234',
group: 'group-1234',
branch: { id: 'branch-1234', name: 'main' },
message: 'test message',
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
};
const codeSigningStub: CodeSigningInfo = {
keyid: 'keyid',
alg: 'alg',
sig: 'sig',
};
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
describe(UpdateRepublish.name, () => {
it('errors without --branch or --group', async () => {
await expect(new UpdateRepublish([], commandOptions).run()).rejects.toThrow(
'--branch or --group must be specified'
);
});
it('errors when update group is empty', async () => {
const flags = ['--group=1234'];
mockTestProject();
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([]);
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
'There are no published updates found'
);
});
it('errors when update group has no update for requested platform', async () => {
const flags = ['--group=1234', '--platform=android'];
mockTestProject();
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
platform: 'ios',
},
]);
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
'There are no updates on branch "main" published for the platform(s) "android" with group ID "1234". Did you mean to publish a new update instead?'
);
});
it('errors when republishing update with both --group and --branch', async () => {
const flags = ['--group=1234', '--branch=main'];
mockTestProject();
await expect(new UpdateRepublish(flags, commandOptions).run()).rejects.toThrow(
/--branch=main cannot also be provided when using --group/
);
});
it('creates a new update from existing update', async () => {
const flags = ['--group=1234', '--message=test-republish'];
mockTestProject();
// Mock queries to retrieve the update and code signing info
jest.mocked(UpdateQuery.viewUpdateGroupAsync).mockResolvedValue([updateStub]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
await new UpdateRepublish(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: updateStub.branch.id,
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: false,
}),
])
);
expect(PublishMutation.setCodeSigningInfoAsync).not.toHaveBeenCalled();
});
it('creates a new update from existing update with codesigning', async () => {
const flags = ['--group=1234', '--message=test-republish'];
const codeSigning = {
alg: 'alg',
keyid: 'keyid',
sig: 'sig',
};
mockTestProject();
// Mock queries to retrieve the update and code signing info
jest
.mocked(UpdateQuery.viewUpdateGroupAsync)
.mockResolvedValue([{ ...updateStub, codeSigningInfo: codeSigningStub }]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
...updateStub,
id: 'update-new',
platform: 'ios',
manifestPermalink: 'https://expo.dev/@test/test-project/manifest',
},
]);
jest.mocked(PublishMutation.setCodeSigningInfoAsync).mockResolvedValue({} as any);
await new UpdateRepublish(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
expect.arrayContaining([
expect.objectContaining({
branchId: updateStub.branch.id,
runtimeVersion: updateStub.runtimeVersion,
updateInfoGroup: expect.objectContaining({
ios: expect.any(Object),
}),
gitCommitHash: updateStub.gitCommitHash,
awaitingCodeSigningInfo: true,
}),
])
);
expect(PublishMutation.setCodeSigningInfoAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
'update-new',
expect.objectContaining(codeSigning)
);
});
});
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
function mockTestProject({
configuredProjectId = '1234',
}: {
configuredProjectId?: string;
} = {}): void {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const appJSON: AppJSONConfig = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: jester.accounts[0].name,
extra: {
eas: {
projectId: configuredProjectId,
},
},
},
};
vol.fromJSON(
{
'package.json': JSON.stringify(packageJSON),
'app.json': JSON.stringify(appJSON),
},
projectRoot
);
const mockManifest = { exp: appJSON.expo };
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest.spyOn(ProjectConfigContextField.prototype, 'getValueAsync').mockResolvedValue({
exp: mockManifest.exp,
projectDir: projectRoot,
projectId: configuredProjectId,
});
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
}
@@ -1,10 +1,11 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
export default class UpdateConfigure extends EasCommand {
static override description = 'configure the project to support EAS Update';
@@ -38,7 +39,7 @@ export default class UpdateConfigure extends EasCommand {
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp,
projectId,
projectDir,
@@ -46,6 +47,23 @@ export default class UpdateConfigure extends EasCommand {
});
Log.addNewLineIfNone();
Log.log(`🎉 Your app is configured to run EAS Update!`);
Log.log(`🎉 Your app is configured with EAS Update!`);
Log.newLine();
Log.log(`${chalk.bold('Next steps')}:`);
Log.newLine();
Log.log('Update a production build:');
Log.log(`1. Create a new build. Example: ${chalk.bold('eas build --profile production')}.`);
Log.log('2. Make changes in your project.');
Log.log(`3. Publish an update. Example: ${chalk.bold('eas update --channel production')}.`);
Log.log('4. Force close and reopen the app at least twice to view the update.');
Log.newLine();
Log.log('Preview an update:');
Log.log(
`1. Publish an update to a branch. Example: ${chalk.bold('eas update --branch new-feature')}.`
);
Log.log(
'2. In Expo Go or a development build, navigate to Projects > [project name] > Branch > Open.'
);
}
}
+133 -186
View File
@@ -17,12 +17,10 @@ import fetch from '../../fetch';
import {
PublishUpdateGroupInput,
StatuspageServiceName,
Update,
UpdateInfoGroup,
UpdatePublishMutation,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
@@ -39,8 +37,8 @@ import {
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
@@ -59,24 +57,26 @@ export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', '
type RawUpdateFlags = {
auto: boolean;
branch?: string;
channel?: string;
message?: string;
group?: string;
republish?: boolean;
platform: string;
'input-dir': string;
'skip-bundler': boolean;
'private-key-path'?: string;
'non-interactive': boolean;
json: boolean;
/** @deprecated see UpdateRepublish command */
group?: string;
/** @deprecated see UpdateRepublish command */
republish?: boolean;
};
type UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
channelName?: string;
updateMessage?: string;
republish: boolean;
groupId?: string;
inputDir: string;
skipBundler: boolean;
privateKeyPath?: string;
@@ -107,16 +107,20 @@ export default class UpdatePublish extends EasCommand {
description: 'Branch to publish the update group on',
required: false,
}),
channel: Flags.string({
description: 'Channel that the published update should affect',
required: false,
}),
message: Flags.string({
description: 'A short message describing the update',
required: false,
}),
republish: Flags.boolean({
description: 'Republish an update group',
description: 'Republish an update group (deprecated, see republish command)',
exclusive: ['input-dir', 'skip-bundler'],
}),
group: Flags.string({
description: 'Update group to republish',
description: 'Update group to republish (deprecated, see republish command)',
exclusive: ['input-dir', 'skip-bundler'],
}),
'input-dir': Flags.string({
@@ -161,16 +165,15 @@ export default class UpdatePublish extends EasCommand {
let {
auto: autoFlag,
platform: platformFlag,
branchName,
channelName,
updateMessage,
republish,
groupId,
inputDir,
skipBundler,
privateKeyPath,
json: jsonFlag,
nonInteractive,
} = this.sanitizeFlags(rawFlags);
let branchName = this.sanitizeFlags(rawFlags).branchName;
const {
getDynamicProjectConfigAsync,
@@ -197,7 +200,7 @@ export default class UpdatePublish extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(platformFlag),
projectDir,
@@ -209,22 +212,31 @@ export default class UpdatePublish extends EasCommand {
let realizedPlatforms: PublishPlatform[] = [];
if (channelName && branchName) {
throw new Error(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
}
if (channelName) {
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
}
if (!branchName) {
if (autoFlag) {
branchName = await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
} else {
try {
const branch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like to ${
republish ? 'republish' : 'publish'
} on?`,
displayTextForListItem: updateBranch =>
`${updateBranch.name} ${chalk.grey(
promptTitle: `Which branch would you like to publish on?`,
displayTextForListItem: updateBranch => ({
title: `${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
}),
paginatedQueryOptions,
});
branchName = branch.name;
@@ -243,156 +255,72 @@ export default class UpdatePublish extends EasCommand {
}
}
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
try {
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
bundleSpinner.fail('Export failed');
throw e;
}
}
// After possibly bundling, assert that the input directory can be found.
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
const assetSpinner = ora().start('Uploading...');
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
let uploadedAssetCount = 0;
let assetLimitPerUpdateGroup = 0;
if (republish) {
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
// Instead we get the `updateInfoGroup` from the update we wish to republish.
let updatesToRepublish: Pick<
Update,
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
>[];
if (groupId) {
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId,
});
updatesToRepublish = updatesByGroup;
} else {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
const uploadResults = await uploadAssetsAsync(
graphqlClient,
assets,
projectId,
(totalAssets, missingAssets) => {
assetSpinner.text = `Uploading (${totalAssets - missingAssets}/${totalAssets})`;
}
updatesToRepublish = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions,
});
}
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
// Only republish to the specified platforms
update => platformFlag === 'all' || update.platform === platformFlag
);
if (updatesToRepublishFilteredByPlatform.length === 0) {
throw new Error(
`There are no updates on branch "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
groupId ? groupId : updatesToRepublish[0].group
}". Did you mean to publish a new update instead?`
);
}
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length < defaultPublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
publicationPlatformMessage = `The republished update will appear on the same platforms it was originally published on: ${updatesToRepublishFilteredByPlatform
.map(update => update.platform)
.join(', ')}`;
} else {
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
}
Log.withTick(publicationPlatformMessage);
for (const update of updatesToRepublishFilteredByPlatform) {
const { manifestFragment } = update;
const platform = update.platform as PublishPlatform;
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
realizedPlatforms = updatesToRepublishFilteredByPlatform.map(
update => update.platform as PublishPlatform
);
// These are the same for each member of an update group
groupId = updatesToRepublishFilteredByPlatform[0].group;
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: `Republish "${oldMessage!}" - group: ${groupId}`,
validate: (value: any) => (value ? true : validationMessage),
}));
}
} else {
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
try {
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
bundleSpinner.fail('Export failed');
throw e;
}
}
// After possibly bundling, assert that the input directory can be found.
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
const assetSpinner = ora().start('Uploading...');
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
const uploadResults = await uploadAssetsAsync(
graphqlClient,
assets,
projectId,
(totalAssets, missingAssets) => {
assetSpinner.text = `Uploading (${totalAssets - missingAssets}/${totalAssets})`;
}
);
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
}
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
}
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
@@ -412,18 +340,24 @@ export default class UpdatePublish extends EasCommand {
}
}
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const gitCommitHash = await getVcsClient().getCommitHashAsync();
const vcsClient = getVcsClient();
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
@@ -435,17 +369,13 @@ export default class UpdatePublish extends EasCommand {
])
);
if (republish && !oldRuntimeVersion) {
throw new Error(
'Cannot find the runtime version of the update group that is being republished.'
);
}
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtimeVersion,
runtimeVersion,
message: truncatedMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
@@ -547,7 +477,14 @@ export default class UpdatePublish extends EasCommand {
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: truncatedMessage },
...(gitCommitHash ? [{ label: 'Commit', value: gitCommitHash }] : []),
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'Website link', value: updateGroupLink },
])
);
@@ -571,26 +508,36 @@ export default class UpdatePublish extends EasCommand {
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && updateMessage)) {
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && channelName && updateMessage)) {
Errors.error(
'--auto or both --branch and --message are required when updating in non-interactive mode',
'--auto or both --channel or --branch and --message are required when updating in non-interactive mode',
{ exit: 1 }
);
}
const groupId = flags.group;
const republish = flags.republish || !!groupId; // When --group is defined, we are republishing
if (nonInteractive && republish && !groupId) {
Errors.error(`--group is required when updating in non-interactive mode`, { exit: 1 });
if (flags.group || flags.republish) {
// Pick the first flag set that is defined, in this specific order
const args = [
['--group', flags.group],
['--branch', flags.branch],
].filter(([_, value]) => value)[0];
Log.newLine();
Log.warn(
'The --group and --republish flags are deprecated, use the republish command instead:'
);
Log.warn(` ${chalk.bold([`eas update:republish`, ...(args ?? [])].join(' '))}`);
Log.newLine();
Errors.error('--group and --republish flags are deprecated', { exit: 1 });
}
return {
auto,
branchName,
channelName,
updateMessage,
groupId,
republish,
inputDir: flags['input-dir'],
skipBundler: flags['skip-bundler'],
platform: flags.platform as RequestedPlatform,
+3 -1
View File
@@ -70,7 +70,9 @@ export default class UpdateList extends EasCommand {
const selectedBranch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
paginatedQueryOptions:
// discard limit and offset because this query is not those flag's intended target
{
@@ -0,0 +1,333 @@
import { Platform } from '@expo/config';
import { Flags } from '@oclif/core';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { Update } from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { truncateString as truncateUpdateMessage } from '../../update/utils';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
const defaultRepublishPlatforms: Platform[] = ['android', 'ios'];
type UpdateRepublishRawFlags = {
branch?: string;
group?: string;
message?: string;
platform: string;
'non-interactive': boolean;
json?: boolean;
};
type UpdateRepublishFlags = {
branchName?: string;
groupId?: string;
updateMessage?: string;
platform: Platform[];
nonInteractive: boolean;
json: boolean;
};
type UpdateToRepublish = {
groupId: string;
branchId: string;
branchName: string;
} & Pick<
Update,
| 'message'
| 'runtimeVersion'
| 'manifestFragment'
| 'platform'
| 'gitCommitHash'
| 'codeSigningInfo'
>;
export default class UpdateRepublish extends EasCommand {
static override description = 'rollback to an existing update';
static override flags = {
branch: Flags.string({
description: 'Branch name to select an update to republish from',
exclusive: ['group'],
}),
group: Flags.string({
description: 'Update group ID to republish',
exclusive: ['branch'],
}),
message: Flags.string({
description: 'Short message describing the republished update',
required: false,
}),
platform: Flags.enum({
char: 'p',
options: [...defaultRepublishPlatforms, 'all'],
default: 'all',
required: false,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdateRepublish);
const flags = this.sanitizeFlags(rawFlags);
const {
projectConfig: { exp, projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(UpdateRepublish, {
nonInteractive: flags.nonInteractive,
});
if (flags.json) {
enableJsonOutput();
}
const existingUpdates = await getOrAskUpdatesAsync(graphqlClient, projectId, flags);
const updatesToPublish = existingUpdates.filter(update =>
flags.platform.includes(update.platform as Platform)
);
if (existingUpdates.length === 0) {
throw new Error(`There are no published updates found`);
}
if (updatesToPublish.length === 0) {
throw new Error(
`There are no updates on branch "${
existingUpdates[0].branchName
}" published for the platform(s) "${rawFlags.platform}" with group ID "${
flags.groupId ? flags.groupId : updatesToPublish[0].groupId
}". Did you mean to publish a new update instead?`
);
}
if (rawFlags.platform === 'all') {
Log.withTick(`The republished update will appear only on: ${rawFlags.platform}`);
} else {
const platformsFromUpdates = updatesToPublish.map(update => update.platform);
if (platformsFromUpdates.length < defaultRepublishPlatforms.length) {
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
Log.withTick(
`The republished update will appear on the same platforms it was originally published on: ${platformsFromUpdates.join(
', '
)}`
);
}
// This command only republishes a single update group
// The update group properties are the same for all updates
const { branchId, branchName, runtimeVersion } = updatesToPublish[0];
const updateMessage = await getOrAskUpdateMessageAsync(updatesToPublish, flags);
// If codesigning was created for the original update, we need to add it to the republish
const shouldRepublishWithCodesigning = updatesToPublish.some(update => update.codeSigningInfo);
if (shouldRepublishWithCodesigning) {
Log.withTick(
`The republished update will be signed with the same codesigning as the original update.`
);
}
const publishIndicator = ora('Republishing...').start();
let updatesRepublished: Awaited<ReturnType<typeof PublishMutation.publishUpdateGroupAsync>>;
try {
updatesRepublished = await PublishMutation.publishUpdateGroupAsync(graphqlClient, [
{
branchId,
runtimeVersion,
message: updateMessage,
updateInfoGroup: Object.fromEntries(
updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])
),
gitCommitHash: updatesToPublish[0].gitCommitHash,
awaitingCodeSigningInfo: shouldRepublishWithCodesigning,
},
]);
if (shouldRepublishWithCodesigning) {
const codeSigningByPlatform = Object.fromEntries(
updatesToPublish.map(update => [update.platform, update.codeSigningInfo])
);
await Promise.all(
updatesRepublished.map(async update => {
const codeSigning = codeSigningByPlatform[update.platform];
if (codeSigning) {
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, update.id, codeSigning);
}
})
);
}
publishIndicator.succeed('Republished update');
} catch (error: any) {
publishIndicator.fail('Failed to republish update');
throw error;
}
if (flags.json) {
return printJsonOnlyOutput(updatesRepublished);
}
const updatesRepublishedByPlatform = Object.fromEntries(
updatesRepublished.map(update => [update.platform, update])
);
const updateGroupUrl = getUpdateGroupUrl(
(await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name,
exp.slug,
updatesRepublished[0].group
);
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: updatesRepublished[0].runtimeVersion },
{ label: 'Platform', value: updatesRepublished.map(update => update.platform).join(', ') },
{ label: 'Update Group ID', value: updatesRepublished[0].id },
...(updatesRepublishedByPlatform.android
? [{ label: 'Android update ID', value: updatesRepublishedByPlatform.android.id }]
: []),
...(updatesRepublishedByPlatform.ios
? [{ label: 'iOS update ID', value: updatesRepublishedByPlatform.ios.id }]
: []),
{ label: 'Message', value: updateMessage },
{ label: 'Website link', value: link(updateGroupUrl, { dim: false }) },
])
);
}
sanitizeFlags(rawFlags: UpdateRepublishRawFlags): UpdateRepublishFlags {
const branchName = rawFlags.branch;
const groupId = rawFlags.group;
if (!branchName && !groupId) {
throw new Error('Either --branch or --group must be specified');
}
const platform =
rawFlags.platform === 'all' ? defaultRepublishPlatforms : ([rawFlags.platform] as Platform[]);
return {
branchName,
groupId,
platform,
updateMessage: rawFlags.message,
json: rawFlags.json ?? false,
nonInteractive: rawFlags['non-interactive'],
};
}
}
/** Retrieve the update group from either the update group id, or select from branch name. */
async function getOrAskUpdatesAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
flags: UpdateRepublishFlags
): Promise<UpdateToRepublish[]> {
if (flags.groupId) {
const updateGroups = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: flags.groupId,
});
return updateGroups.map(group => ({
...group,
groupId: group.group,
branchId: group.branch.id,
branchName: group.branch.name,
}));
}
if (flags.branchName) {
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
branchName: flags.branchName,
projectId,
});
}
throw new Error('Must supply --group or --branch');
}
/** Ask the user which update needs to be republished by branch name, this requires interactive mode */
async function askUpdatesFromBranchNameAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
branchName,
json,
nonInteractive,
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
const updateGroups = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions: getPaginatedQueryOptions({ json, 'non-interactive': nonInteractive }),
});
return updateGroups.map(group => ({
...group,
groupId: group.id,
branchId: group.branch.id,
branchName: group.branch.name,
}));
}
/** Get or ask the user for the update (group) message for the republish */
async function getOrAskUpdateMessageAsync(
updates: UpdateToRepublish[],
flags: UpdateRepublishFlags
): Promise<string> {
if (flags.updateMessage) {
return sanitizeUpdateMessage(flags.updateMessage);
}
if (flags.nonInteractive || flags.json) {
throw new Error('Must supply --message when in non-interactive mode');
}
// This command only uses a single update group to republish, meaning these values are always identical
const oldGroupId = updates[0].groupId;
const oldUpdateMessage = updates[0].message;
const { updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: 'Provide an update message.',
initial: `Republish "${oldUpdateMessage!}" - group: ${oldGroupId}`,
validate: (value: any) => (value ? true : 'Update message may not be empty.'),
});
return sanitizeUpdateMessage(updateMessage);
}
function sanitizeUpdateMessage(updateMessage: string): string {
if (updateMessage !== truncateUpdateMessage(updateMessage, 1024)) {
Log.warn('Update message exceeds the allowed 1024 character limit, truncated update message.');
return truncateUpdateMessage(updateMessage, 1024);
}
return updateMessage;
}
+6 -3
View File
@@ -44,10 +44,11 @@ export async function selectAppleTeamOnAccountAsync(
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleTeam =>
appleTeam.appleTeamName
createDisplayTextForSelectionPromptListItem: appleTeam => ({
title: appleTeam.appleTeamName
? `${appleTeam.appleTeamName} (ID: ${appleTeam.appleTeamIdentifier})`
: appleTeam.appleTeamIdentifier,
}),
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
@@ -87,7 +88,9 @@ export async function selectAppleDeviceOnAppleTeamAsync(
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleDevice => formatDeviceLabel(appleDevice),
createDisplayTextForSelectionPromptListItem: appleDevice => ({
title: formatDeviceLabel(appleDevice),
}),
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
@@ -23,13 +23,12 @@ function formatDeviceClass(device: Device | NewDevice): string {
export default function formatDevice(device: Device, team?: AppleTeamIdAndName): string {
const fields: FormatFieldsItem[] = [
{ label: 'ID', value: device.id },
{ label: 'UDID', value: device.identifier },
{ label: 'Name', value: device.name ?? 'Unknown' },
{
label: 'Class',
value: formatDeviceClass(device),
},
{ label: 'UDID', value: device.identifier },
];
if (team) {
+99 -14
View File
@@ -400,7 +400,6 @@ export type AccountSsoConfiguration = {
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
@@ -415,7 +414,6 @@ export type AccountSsoConfigurationData = {
clientSecret: Scalars['String'];
issuer?: InputMaybe<Scalars['String']>;
jwksEndpoint?: InputMaybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: InputMaybe<Scalars['String']>;
tokenEndpoint?: InputMaybe<Scalars['String']>;
userInfoEndpoint?: InputMaybe<Scalars['String']>;
@@ -458,7 +456,6 @@ export type AccountSsoConfigurationPublicData = {
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
userInfoEndpoint?: Maybe<Scalars['String']>;
@@ -787,6 +784,7 @@ export type AndroidJobInput = {
developmentClient?: InputMaybe<Scalars['Boolean']>;
experimental?: InputMaybe<Scalars['JSONObject']>;
gradleCommand?: InputMaybe<Scalars['String']>;
mode?: InputMaybe<BuildMode>;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: InputMaybe<Scalars['String']>;
@@ -815,6 +813,7 @@ export type AndroidJobOverridesInput = {
developmentClient?: InputMaybe<Scalars['Boolean']>;
experimental?: InputMaybe<Scalars['JSONObject']>;
gradleCommand?: InputMaybe<Scalars['String']>;
mode?: InputMaybe<BuildMode>;
releaseChannel?: InputMaybe<Scalars['String']>;
secrets?: InputMaybe<AndroidJobSecretsInput>;
updates?: InputMaybe<BuildUpdatesInput>;
@@ -922,6 +921,7 @@ export type App = Project & {
/** Classic update release channel names that have at least one build */
buildsReleaseChannels: Array<Scalars['String']>;
deployment?: Maybe<Deployment>;
deploymentNew?: Maybe<DeploymentNew>;
/** Deployments associated with this app */
deployments: Array<Deployment>;
description: Scalars['String'];
@@ -1051,6 +1051,13 @@ export type AppDeploymentArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentNewArgs = {
channel: Scalars['String'];
runtimeVersion: Scalars['String'];
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsArgs = {
limit: Scalars['Int'];
@@ -1800,6 +1807,12 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
workerStartedAt?: Maybe<Scalars['DateTime']>;
};
/** Represents an EAS Build */
export type BuildCanRetryArgs = {
newMode?: InputMaybe<BuildMode>;
};
export type BuildArtifact = {
__typename?: 'BuildArtifact';
manifestPlistUrl?: Maybe<Scalars['String']>;
@@ -1960,6 +1973,11 @@ export type BuildMetrics = {
buildWaitTime?: Maybe<Scalars['Int']>;
};
export enum BuildMode {
Build = 'BUILD',
Resign = 'RESIGN'
}
export type BuildMutation = {
__typename?: 'BuildMutation';
/**
@@ -2116,6 +2134,10 @@ export type BuildQueryByIdArgs = {
buildId: Scalars['ID'];
};
export type BuildResignInput = {
applicationArchiveSource?: InputMaybe<ProjectArchiveSourceInput>;
};
export enum BuildResourceClass {
AndroidDefault = 'ANDROID_DEFAULT',
AndroidLarge = 'ANDROID_LARGE',
@@ -2141,7 +2163,8 @@ export type BuildUpdatesInput = {
export enum BuildWorkflow {
Generic = 'GENERIC',
Managed = 'MANAGED'
Managed = 'MANAGED',
Unknown = 'UNKNOWN'
}
export enum CacheControlScope {
@@ -2381,6 +2404,36 @@ export type Deployment = {
runtimeVersion: Scalars['String'];
};
export type DeploymentBuildEdge = {
__typename?: 'DeploymentBuildEdge';
cursor: Scalars['String'];
node: Build;
};
export type DeploymentBuildsConnection = {
__typename?: 'DeploymentBuildsConnection';
edges: Array<DeploymentBuildEdge>;
pageInfo: PageInfo;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNew = {
__typename?: 'DeploymentNew';
builds: DeploymentBuildsConnection;
channel: UpdateChannel;
id: Scalars['ID'];
runtime: Runtime;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNewBuildsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
export type DeploymentOptions = {
/** Max number of associated builds to return */
buildListMaxSize?: InputMaybe<Scalars['Int']>;
@@ -2547,7 +2600,7 @@ export type GitHubAppInstallationAccessibleRepositoriesArgs = {
export type GitHubAppInstallationAccessibleRepository = {
__typename?: 'GitHubAppInstallationAccessibleRepository';
defaultBranch: Scalars['String'];
defaultBranch?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
id: Scalars['Int'];
name: Scalars['String'];
@@ -2982,6 +3035,7 @@ export type IosJobInput = {
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
experimental?: InputMaybe<Scalars['JSONObject']>;
mode?: InputMaybe<BuildMode>;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: InputMaybe<Scalars['String']>;
@@ -3008,10 +3062,13 @@ export type IosJobOverridesInput = {
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
experimental?: InputMaybe<Scalars['JSONObject']>;
mode?: InputMaybe<BuildMode>;
releaseChannel?: InputMaybe<Scalars['String']>;
resign?: InputMaybe<BuildResignInput>;
scheme?: InputMaybe<Scalars['String']>;
secrets?: InputMaybe<IosJobSecretsInput>;
simulator?: InputMaybe<Scalars['Boolean']>;
type?: InputMaybe<BuildWorkflow>;
updates?: InputMaybe<BuildUpdatesInput>;
username?: InputMaybe<Scalars['String']>;
version?: InputMaybe<IosJobVersionInput>;
@@ -3249,6 +3306,14 @@ export enum Order {
Desc = 'DESC'
}
export type PageInfo = {
__typename?: 'PageInfo';
endCursor?: Maybe<Scalars['String']>;
hasNextPage: Scalars['Boolean'];
hasPreviousPage: Scalars['Boolean'];
startCursor?: Maybe<Scalars['String']>;
};
export type PartialManifest = {
assets: Array<InputMaybe<PartialManifestAsset>>;
extra?: InputMaybe<Scalars['JSONObject']>;
@@ -3298,6 +3363,7 @@ export type ProjectArchiveSourceInput = {
export enum ProjectArchiveSourceType {
Gcs = 'GCS',
None = 'NONE',
S3 = 'S3',
Url = 'URL'
}
@@ -3347,7 +3413,9 @@ export type PublicArtifacts = {
export type PublishUpdateGroupInput = {
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']>;
branchId: Scalars['String'];
excludedAssets?: InputMaybe<Array<PartialManifestAsset>>;
gitCommitHash?: InputMaybe<Scalars['String']>;
isGitWorkingTreeDirty?: InputMaybe<Scalars['Boolean']>;
message?: InputMaybe<Scalars['String']>;
runtimeVersion: Scalars['String'];
updateInfoGroup: UpdateInfoGroup;
@@ -3630,6 +3698,14 @@ export type RootQueryUserByUsernameArgs = {
username: Scalars['String'];
};
export type Runtime = {
__typename?: 'Runtime';
app: App;
firstBuildCreatedAt: Scalars['DateTime'];
id: Scalars['ID'];
version: Scalars['String'];
};
export type SecondFactorBooleanResult = {
__typename?: 'SecondFactorBooleanResult';
success: Scalars['Boolean'];
@@ -4007,6 +4083,7 @@ export type Update = ActivityTimelineProjectActivity & {
gitCommitHash?: Maybe<Scalars['String']>;
group: Scalars['String'];
id: Scalars['ID'];
isGitWorkingTreeDirty: Scalars['Boolean'];
manifestFragment: Scalars['String'];
manifestPermalink: Scalars['String'];
message?: Maybe<Scalars['String']>;
@@ -5029,6 +5106,14 @@ export type CreateIosBuildMutationVariables = Exact<{
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type RetryIosBuildMutationVariables = Exact<{
buildId: Scalars['ID'];
jobOverrides: IosJobOverridesInput;
}>;
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
accountId: Scalars['String'];
@@ -5167,7 +5252,7 @@ export type BranchesByAppQueryVariables = Exact<{
}>;
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }> }> } } };
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> }> } } };
export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5177,7 +5262,7 @@ export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
}>;
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> }> } | null } } };
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
export type BuildsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
@@ -5209,7 +5294,7 @@ export type ViewUpdateChannelOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> }> } | null } } };
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5218,7 +5303,7 @@ export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> }> }> } } };
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> }> } } };
export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5271,7 +5356,7 @@ export type ViewUpdatesByGroupQueryVariables = Exact<{
}>;
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }> };
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5282,7 +5367,7 @@ export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
}>;
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> } | null } } };
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } | null } } };
export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5292,7 +5377,7 @@ export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }>> } } };
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } } };
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
@@ -5328,9 +5413,9 @@ export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id:
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } };
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null };
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string } }> };
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
export type WebhookFragment = { __typename?: 'Webhook', id: string, event: WebhookType, url: string, createdAt: any, updatedAt: any };
@@ -15,6 +15,9 @@ import {
CreateIosBuildMutationVariables,
EasBuildDeprecationInfo,
IosJobInput,
IosJobOverridesInput,
RetryIosBuildMutation,
RetryIosBuildMutationVariables,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
@@ -116,4 +119,32 @@ export const BuildMutation = {
);
return nullthrows(data.build?.createIosBuild);
},
async retryIosBuildAsync(
graphqlClient: ExpoGraphqlClient,
input: {
buildId: string;
jobOverrides: IosJobOverridesInput;
}
): Promise<BuildFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<RetryIosBuildMutation, RetryIosBuildMutationVariables>(
gql`
mutation RetryIosBuildMutation($buildId: ID!, $jobOverrides: IosJobOverridesInput!) {
build {
retryIosBuild(buildId: $buildId, jobOverrides: $jobOverrides) {
id
...BuildFragment
}
}
}
${print(BuildFragmentNode)}
`,
input,
{ noRetry: true }
)
.toPromise()
);
return nullthrows(data.build?.retryIosBuild);
},
};
@@ -1,6 +1,7 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ChannelNotFoundError } from '../../channel/errors';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
@@ -59,7 +60,7 @@ export const ChannelQuery = {
const { updateChannelByName } = response.app.byId;
if (!updateChannelByName) {
throw new Error(`Could not find channel with the name ${channelName}`);
throw new ChannelNotFoundError(`Could not find channel with the name ${channelName}`);
}
return updateChannelByName;
@@ -9,6 +9,7 @@ export const UpdateFragmentNode = gql`
runtimeVersion
platform
manifestFragment
gitCommitHash
actor {
__typename
id
@@ -23,5 +24,10 @@ export const UpdateFragmentNode = gql`
id
name
}
codeSigningInfo {
keyid
sig
alg
}
}
`;
@@ -1,67 +0,0 @@
import { ProjectConfig, getConfig } from '@expo/config';
import { getExpoConfig } from '../expoConfig';
jest.mock('@expo/config');
describe(getExpoConfig, () => {
it('correctly restores process.env as best as possible', () => {
process.env.BEFORE_CALL_KEY = 'BEFORE';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN = 'BEFORE_OVERRIDDEN_BY_WITHIN';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS = 'BEFORE_OVERRIDDEN_BY_OPTS';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN =
'BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN';
jest.mocked(getConfig).mockImplementation(() => {
process.env.WITHIN_CALL_KEY = 'WITHIN';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN = 'OVERRIDDEN_VALUE';
process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN = 'OVERRIDDEN_VALUE';
process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE =
'OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN = 'OVERRIDDEN_WITHIN';
return {} as ProjectConfig;
});
// ensure idempotent (run N times)
for (let i = 0; i < 2; i++) {
getExpoConfig('', {
env: {
OPTS_KEY: 'OPTS',
BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS: 'OVERRIDDEN_VALUE',
OPTS_KEY_OVERRIDDEN_BY_WITHIN: 'OPTS_KEY_OVERRIDDEN_BY_WITHIN',
OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE:
'OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE',
BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN: 'OVERRIDDEN_OPTS',
},
});
// env from before the call should be restored
expect(process.env.BEFORE_CALL_KEY).toEqual('BEFORE');
// env from before the call but overridden within the call should not be restored
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN).toEqual('OVERRIDDEN_VALUE');
// env from before the call but overridden by opts should be restored
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS).toEqual('BEFORE_OVERRIDDEN_BY_OPTS');
// env added within the call should be kept
expect(process.env.WITHIN_CALL_KEY).toEqual('WITHIN');
// env added by opts should be removed
expect(process.env.OPTS_KEY).toBeUndefined();
// env added by opts but overridden within the call should be retained and have overridden value
expect(process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN).toEqual('OVERRIDDEN_VALUE');
// env added by opts but overridden within the call to the same value will be removed
// note that this is a known bug/limitation of this approach
expect(process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE).toBeUndefined();
// env from before but overridden within the call and by opts should retain value from
// within the call
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN).toEqual(
'OVERRIDDEN_WITHIN'
);
}
});
});
@@ -8,7 +8,10 @@ import {
getApplicationIdAsync,
} from './android/applicationId';
import { resolveGradleBuildContextAsync } from './android/gradle';
import { getBundleIdentifierAsync } from './ios/bundleIdentifier';
import {
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
getBundleIdentifierAsync,
} from './ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from './ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from './ios/target';
import { resolveWorkflowAsync } from './workflow';
@@ -23,15 +26,20 @@ export async function getApplicationIdentifierAsync(
if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>;
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile);
if (workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp, actor);
return await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp, actor);
}
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile);
return await getApplicationIdAsync(projectDir, exp, gradleContext);
} else {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const profile = buildProfile as BuildProfile<Platform.IOS>;
if (workflow === Workflow.MANAGED) {
return await ensureBundleIdentifierIsDefinedForManagedProjectAsync(projectDir, exp, actor);
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{ exp, projectDir, nonInteractive: false },
profile
+1 -25
View File
@@ -1,6 +1,5 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
export interface ExpoConfigOptions {
env?: Env;
@@ -10,11 +9,6 @@ export interface ExpoConfigOptions {
export function getExpoConfig(projectDir: string, opts: ExpoConfigOptions = {}): ExpoConfig {
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
const optsEnvKeys = opts.env ? Object.keys(opts.env) : [];
const originalEnvKeys = new Set(Object.keys(process.env));
const newKeysAddedToProcessEnv = new Set(optsEnvKeys.filter(k => !originalEnvKeys.has(k)));
try {
process.env = {
...process.env,
@@ -26,24 +20,6 @@ export function getExpoConfig(projectDir: string, opts: ExpoConfigOptions = {}):
});
return exp;
} finally {
optsEnvKeys.forEach(k => {
// if new key added, remove unless it was modified
if (newKeysAddedToProcessEnv.has(k)) {
if (k in originalProcessEnv) {
if (process.env[k] === originalProcessEnv[k]) {
delete process.env[k];
}
} else {
if (nullthrows(opts.env)[k] === process.env[k]) {
delete process.env[k];
}
}
} else {
// opts key overrode env key, but only if the value is equal to the one declared by opts
if (process.env[k] === nullthrows(opts.env)[k]) {
process.env[k] = originalProcessEnv[k];
}
}
});
process.env = originalProcessEnv;
}
}
+57
View File
@@ -0,0 +1,57 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import glob from 'fast-glob';
import path from 'path';
import Log from '../../log';
import { getAndroidSdkRootAsync } from './sdk';
async function aaptAsync(...options: string[]): Promise<SpawnResult> {
try {
return await spawnAsync(await getAaptExecutableAsync(), options);
} catch (error: any) {
if (error.stderr) {
Log.error(error.stderr);
}
throw error;
}
}
export async function getAaptExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (!sdkRoot) {
Log.debug('Failed to resolve the Android SDK path, falling back to global aapt executable');
return 'aapt';
}
const aaptPaths = await glob(
path.posix.join('build-tools/**', process.platform === 'win32' ? 'aapt.exe' : 'aapt'),
{ cwd: sdkRoot, absolute: true }
);
if (aaptPaths.length === 0) {
throw new Error('Failed to resolve the Android aapt path');
}
const sorted = aaptPaths.sort();
return sorted[sorted.length - 1];
}
export async function getAptParametersAsync(
appPath: string
): Promise<{ packageName: string; activityName: string }> {
const { stdout } = await aaptAsync('dump', 'badging', appPath);
const packageNameMatch = stdout.match(/package: name='([^']+)'/);
if (!packageNameMatch) {
throw new Error(`Could not read package name from ${appPath}`);
}
// get activity name
const activityNameMatch = stdout.match(/launchable-activity: name='([^']+)'/);
if (!activityNameMatch) {
throw new Error(`Could not read activity name from ${appPath}`);
}
return {
packageName: packageNameMatch[1],
activityName: activityNameMatch[1],
};
}
+132
View File
@@ -0,0 +1,132 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import os from 'os';
import path from 'path';
import Log from '../../log';
import { truthy } from '../../utils/expodash/filter';
import { sleepAsync } from '../../utils/promise';
import { getAndroidSdkRootAsync } from './sdk';
export interface AndroidEmulator {
pid?: string;
name: string;
}
const BEGINNING_OF_ADB_ERROR_MESSAGE = 'error: ';
export async function adbAsync(...args: string[]): Promise<SpawnResult> {
const adbExecutable = await getAdbExecutableAsync();
try {
return await spawnAsync(adbExecutable, args);
} catch (error: any) {
let errorMessage = (error.stderr || error.stdout || error.message).trim();
if (errorMessage.startsWith(BEGINNING_OF_ADB_ERROR_MESSAGE)) {
errorMessage = errorMessage.substring(BEGINNING_OF_ADB_ERROR_MESSAGE.length);
}
error.message = errorMessage;
throw error;
}
}
export async function getAdbExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (!sdkRoot) {
Log.debug('Failed to resolve the Android SDK path, falling back to global adb executable');
return 'adb';
}
return path.join(sdkRoot, 'platform-tools/adb');
}
export function sanitizeAdbDeviceName(deviceName: string): string | undefined {
return deviceName.trim().split(/[\r\n]+/)[0];
}
/**
* Return the Emulator name for an emulator ID, this can be used to determine if an emulator is booted.
*
* @param devicePid a value like `emulator-5554` from `abd devices`
*/
export async function getAdbNameForDeviceIdAsync(emulatorPid: string): Promise<string | null> {
const { stdout } = await adbAsync('-s', emulatorPid, 'emu', 'avd', 'name');
if (stdout.match(/could not connect to TCP port .*: Connection refused/)) {
// Can also occur when the emulator does not exist.
throw new Error(`Emulator not found: ${stdout}`);
}
return sanitizeAdbDeviceName(stdout) ?? null;
}
// TODO: This is very expensive for some operations.
export async function getRunningEmulatorsAsync(): Promise<AndroidEmulator[]> {
const { stdout } = await adbAsync('devices', '-l');
const splitItems = stdout.trim().split(os.EOL);
const attachedDevices: {
pid: string;
type: string;
}[] = splitItems
// First line is `"List of devices attached"`, remove it
.slice(1, splitItems.length)
.map(line => {
// unauthorized: ['FA8251A00719', 'unauthorized', 'usb:338690048X', 'transport_id:5']
// authorized: ['FA8251A00719', 'device', 'usb:336592896X', 'product:walleye', 'model:Pixel_2', 'device:walleye', 'transport_id:4']
// emulator: ['emulator-5554', 'offline', 'transport_id:1']
const [pid] = line.split(' ').filter(truthy);
const type = line.includes('emulator') ? 'emulator' : 'device';
return { pid, type };
})
.filter(({ pid, type }) => !!pid && type === 'emulator');
const devicePromises = attachedDevices.map<Promise<AndroidEmulator>>(async ({ pid }) => {
const name = (await getAdbNameForDeviceIdAsync(pid)) ?? '';
return {
pid,
name,
};
});
return Promise.all(devicePromises);
}
export async function getFirstRunningEmulatorAsync(): Promise<AndroidEmulator | null> {
const emulators = await getRunningEmulatorsAsync();
return emulators[0] ?? null;
}
/**
* Returns true if emulator is booted
*
* @param emulatorPid
*/
export async function isEmulatorBootedAsync(emulatorPid: string): Promise<boolean> {
try {
const { stdout } = await adbAsync('-s', emulatorPid, 'shell', 'getprop', 'sys.boot_completed');
if (stdout.trim() === '1') {
return true;
}
return false;
} catch {
return false;
}
}
export async function waitForEmulatorToBeBootedAsync(
maxWaitTimeMs: number,
intervalMs: number
): Promise<AndroidEmulator> {
Log.newLine();
Log.log('Waiting for the Android emulator to start...');
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTimeMs) {
const emulator = await getFirstRunningEmulatorAsync();
if (emulator?.pid && (await isEmulatorBootedAsync(emulator.pid))) {
return emulator;
}
await sleepAsync(intervalMs);
}
throw new Error('Timed out waiting for the Android emulator to start.');
}
@@ -0,0 +1,160 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import assert from 'assert';
import chalk from 'chalk';
import os from 'os';
import path from 'path';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { truthy } from '../../utils/expodash/filter';
import {
AndroidEmulator,
adbAsync,
getFirstRunningEmulatorAsync,
isEmulatorBootedAsync,
waitForEmulatorToBeBootedAsync,
} from './adb';
import { getAndroidSdkRootAsync } from './sdk';
export const EMULATOR_MAX_WAIT_TIMEOUT_MS = 60 * 1000 * 3;
export async function getEmulatorExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (sdkRoot) {
return path.join(sdkRoot, 'emulator', 'emulator');
}
return 'emulator';
}
async function emulatorAsync(...options: string[]): Promise<SpawnResult> {
const emulatorExecutable = await getEmulatorExecutableAsync();
try {
return await spawnAsync(emulatorExecutable, options);
} catch (error: any) {
if (error.stderr) {
Log.error(error.stderr);
}
throw error;
}
}
async function getAvaliableAndroidEmulatorsAsync(): Promise<AndroidEmulator[]> {
try {
const { stdout } = await emulatorAsync('-list-avds');
return stdout
.split(os.EOL)
.filter(truthy)
.map(name => ({
name,
}));
} catch {
return [];
}
}
/** Start an Android device and wait until it is booted. */
async function bootEmulatorAsync(
emulator: AndroidEmulator,
{
timeout = EMULATOR_MAX_WAIT_TIMEOUT_MS,
interval = 1000,
}: {
/** Time in milliseconds to wait before asserting a timeout error. */
timeout?: number;
interval?: number;
} = {}
): Promise<AndroidEmulator> {
Log.newLine();
Log.log(`Opening emulator ${chalk.bold(emulator.name)}`);
const emulatorExecutable = await getEmulatorExecutableAsync();
// Start a process to open an emulator
const emulatorProcess = spawnAsync(emulatorExecutable, [`@${emulator.name}`], {
stdio: 'ignore',
detached: true,
});
// we don't want to wait for the emulator process to exit before we can finish `eas build:run` command
// https://github.com/expo/eas-cli/pull/1485#discussion_r1007935871
emulatorProcess.child.unref();
return await waitForEmulatorToBeBootedAsync(timeout, interval);
}
export async function selectEmulatorAsync(): Promise<AndroidEmulator> {
const runningEmulator = await getFirstRunningEmulatorAsync();
if (runningEmulator) {
Log.newLine();
Log.log(`Using open emulator: ${chalk.bold(runningEmulator.name)}`);
return runningEmulator;
}
const emulators = await getAvaliableAndroidEmulatorsAsync();
Log.newLine();
const { selectedEmulator } = await promptAsync({
type: 'select',
message: `Select an emulator to run your app on`,
name: 'selectedEmulator',
choices: emulators.map(emulator => ({
title: emulator.name,
value: emulator,
})),
});
return selectedEmulator;
}
export async function ensureEmulatorBootedAsync(
emulator: AndroidEmulator
): Promise<AndroidEmulator> {
if (!emulator.pid || !(await isEmulatorBootedAsync(emulator.pid))) {
return await bootEmulatorAsync(emulator);
}
return emulator;
}
export async function installAppAsync(
emulator: AndroidEmulator,
apkFilePath: string
): Promise<void> {
Log.newLine();
Log.log('Installing your app...');
assert(emulator.pid);
await adbAsync('-s', emulator.pid, 'install', '-r', '-d', apkFilePath);
Log.succeed('Successfully installed your app!');
}
export async function startAppAsync(
emulator: AndroidEmulator,
packageName: string,
activityName: string
): Promise<void> {
Log.newLine();
Log.log('Starting your app...');
assert(emulator.pid);
await adbAsync(
'-s',
emulator.pid,
'shell',
'am',
'start',
'-a',
'android.intent.action.MAIN',
'-f',
'0x20000000', // FLAG_ACTIVITY_SINGLE_TOP -- If set, the activity will not be launched if it is already running at the top of the history stack.
'-n',
`${packageName}/${activityName}`
);
Log.succeed('Successfully started your app!');
}
+10 -3
View File
@@ -1,5 +1,12 @@
import Log from '../../log';
import { getAptParametersAsync } from './aapt';
import * as emulator from './emulator';
import { assertExecutablesExistAsync } from './systemRequirements';
export async function runAppOnAndroidEmulatorAsync(_appPath: string): Promise<void> {
Log.error('Not implemented!');
export async function runAppOnAndroidEmulatorAsync(appPath: string): Promise<void> {
await assertExecutablesExistAsync();
const selectedEmulator = await emulator.selectEmulatorAsync();
const bootedEmulator = await emulator.ensureEmulatorBootedAsync(selectedEmulator);
await emulator.installAppAsync(bootedEmulator, appPath);
const { packageName, activityName } = await getAptParametersAsync(appPath);
await emulator.startAppAsync(bootedEmulator, packageName, activityName);
}
+26
View File
@@ -0,0 +1,26 @@
import { pathExists } from 'fs-extra';
import os from 'os';
import path from 'path';
export const ANDROID_DEFAULT_LOCATION: Readonly<Partial<Record<NodeJS.Platform, string>>> = {
darwin: path.join(os.homedir(), 'Library', 'Android', 'sdk'),
linux: path.join(os.homedir(), 'Android', 'Sdk'),
win32: path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
};
const ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM = ANDROID_DEFAULT_LOCATION[process.platform];
export async function getAndroidSdkRootAsync(): Promise<string | null> {
if (process.env.ANDROID_HOME && (await pathExists(process.env.ANDROID_HOME))) {
return process.env.ANDROID_HOME;
} else if (process.env.ANDROID_SDK_ROOT && (await pathExists(process.env.ANDROID_SDK_ROOT))) {
return process.env.ANDROID_SDK_ROOT;
} else if (
ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM &&
(await pathExists(ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM))
) {
return ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM;
} else {
return null;
}
}
@@ -0,0 +1,27 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import { getAaptExecutableAsync } from './aapt';
import { getAdbExecutableAsync } from './adb';
import { getEmulatorExecutableAsync } from './emulator';
async function assertExecutableExistsAsync(executable: string, options?: string[]): Promise<void> {
try {
await spawnAsync(executable, options);
} catch (err: any) {
throw new Error(
`${chalk.bold(
executable
)} executable doesn't seem to work. Please make sure Android Studio is installed on your device and ${chalk.bold(
'ANDROID_HOME'
)} or ${chalk.bold('ANDROID_SDK_ROOT')} env variables are set.
${err.message}`
);
}
}
export async function assertExecutablesExistAsync(): Promise<void> {
await assertExecutableExistsAsync(await getAdbExecutableAsync(), ['--version']);
await assertExecutableExistsAsync(await getEmulatorExecutableAsync(), ['-list-avds']);
await assertExecutableExistsAsync(await getAaptExecutableAsync(), ['version']);
}
+1 -1
View File
@@ -33,7 +33,7 @@ export async function selectSimulatorAsync(): Promise<IosSimulator> {
message: `Select a simulator to run your app on`,
name: 'selectedSimulator',
choices: simulators.map(simulator => ({
title: `ios ${simulator.osVersion} ${simulator.name}`,
title: `iOS ${simulator.osVersion} ${simulator.name}`,
value: simulator,
})),
});
+19
View File
@@ -0,0 +1,19 @@
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../graphql/generated';
function isAab(build: BuildFragment): boolean {
return build.artifacts?.applicationArchiveUrl?.endsWith('.aab') ?? false;
}
function didArtifactsExpire(build: BuildFragment): boolean {
return new Date().getTime() - new Date(build.updatedAt).getTime() > 30 * 24 * 60 * 60 * 1000; // 30 days
}
export function isRunnableOnSimulatorOrEmulator(build: BuildFragment): boolean {
return (
build.status === BuildStatus.Finished &&
!!build.artifacts?.applicationArchiveUrl &&
((build.platform === AppPlatform.Ios && build.distribution === DistributionType.Simulator) ||
(build.platform === AppPlatform.Android && !isAab(build))) &&
!didArtifactsExpire(build)
);
}
@@ -0,0 +1,155 @@
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { UpdateBranch, UpdateChannel } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { getBranchNameFromChannelNameAsync } from '../getBranchNameFromChannelNameAsync';
jest.mock('../../graphql/queries/ChannelQuery');
jest.mock('../../graphql/queries/BranchQuery');
describe(getBranchNameFromChannelNameAsync, () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('returns branch name when channel exists and has one connected branch', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name'],
}) as any
);
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'channel-name'
);
expect(result).toBe('test-branch-name');
});
test('errors when no branch is connected to channel', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
}) as any
);
expect(
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
);
});
test('errors when more than one branch is connected to channel', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name', 'test-branch-name-2'],
}) as any
);
expect(
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has multiple branches associated with it. Instead, use 'eas update --branch'"
);
});
test('creates channel and branch when channel does not exist, and returns branch name', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name'],
}) as any
);
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'test-channel-name'
);
expect(result).toBe('test-branch-name');
});
});
function mockUpdateChannel({
channelName,
branchNames,
}: {
channelName?: string;
branchNames?: string[];
}): UpdateChannel {
return {
id: 'a2a1fa12-9d6a-433a-a432-49c64ef8439f',
name: channelName ?? 'default-channel-name',
createdAt: '2022-12-07T02:24:29.786Z',
appId: '123',
updatedAt: '2022-12-07T02:24:29.786Z',
branchMapping:
'{"data":[{"branchId":"f9dc4dbb-663f-4a19-8cf2-a783b484d2db","branchMappingLogic":"true"}],"version":0}',
updateBranches: branchNames ? mockUpdateBranches(branchNames) : [],
__typename: 'UpdateChannel',
};
}
function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
return branchNames.map(branchName => ({
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
name: branchName ?? 'default-branch-name',
appId: '123',
createdAt: '2022-12-07T02:24:29.786Z',
updatedAt: '2022-12-07T02:24:29.786Z',
updates: [],
updateGroups: [
[
{
id: '8503286d-4b18-4b38-a0f7-2b9ae09531a0',
group: 'd715bc20-76b5-477b-a060-3f8184aae87a',
message: 'my message',
createdAt: '2022-12-07T02:24:43.487Z',
runtimeVersion: '1.0.0',
platform: 'ios',
manifestFragment: '...',
activityTimestamp: '2022-12-07T02:24:43.487Z',
branchId: '',
awaitingCodeSigningInfo: false,
isGitWorkingTreeDirty: false,
manifestPermalink: '...',
updatedAt: '2022-12-07T02:24:43.487Z',
gitCommitHash: 'abc5baf77eee821b800af97879d19cc3132b46a7',
actor: {
__typename: 'User' as const,
id: 'f42b73a3-ce6c-4059-a825-e5ddafa022f7',
username: 'jonexpo',
} as any,
branch: {
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
name: 'production3',
appId: '123',
createdAt: '2022-12-07T02:24:29.786Z',
updatedAt: '2022-12-07T02:24:29.786Z',
updateGroups: [],
updates: [],
__typename: 'UpdateBranch' as const,
},
codeSigningInfo: null,
__typename: 'Update' as const,
},
],
],
__typename: 'UpdateBranch' as const,
}));
}
+68 -3
View File
@@ -1,7 +1,9 @@
import { modifyConfigAsync } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { getEASUpdateURL } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -225,7 +227,7 @@ function warnEASUpdatesManualConfig({
/**
* Make sure that the current `app.json` configuration for EAS Updates is set natively.
*/
async function ensureEASUpdatesIsConfiguredNativelyAsync(
async function ensureEASUpdateIsConfiguredNativelyAsync(
graphqlClient: ExpoGraphqlClient,
{
exp,
@@ -252,6 +254,67 @@ async function ensureEASUpdatesIsConfiguredNativelyAsync(
}
}
/**
* Make sure EAS Build profiles are configured to work with EAS Update by adding channels to build profiles.
*/
async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
Log.warn(
`EAS Build is not configured. If you'd like to use EAS Build with EAS Update, run ${chalk.bold(
'eas build:configure'
)}, then re-run ${chalk.bold('eas update:configure')} to configure ${chalk.bold(
'eas.json'
)} with EAS Update.`
);
return;
}
try {
const easJsonAccessor = new EasJsonAccessor(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
const easBuildProfilesWithChannels = Object.keys(easJsonRawObject.build).reduce(
(acc, profileNameKey) => {
const buildProfile = easJsonRawObject.build[profileNameKey];
const isNotAlreadyConfigured = !buildProfile.channel && !buildProfile.releaseChannel;
if (isNotAlreadyConfigured) {
return {
...acc,
[profileNameKey]: {
...buildProfile,
channel: profileNameKey,
},
};
}
return {
...acc,
[profileNameKey]: {
...easJsonRawObject.build[profileNameKey],
},
};
},
{}
);
return {
...easJsonRawObject,
build: easBuildProfilesWithChannels,
};
});
await easJsonAccessor.writeAsync();
Log.withTick(`Configured ${chalk.bold('eas.json')}.`);
} catch (error) {
Log.error(`We were not able to configure ${chalk.bold('eas.json')}. Error: ${error}.`);
}
}
/**
* Make sure EAS Update is fully configured in the current project.
* This goes over a checklist and performs the following checks or changes:
@@ -261,7 +324,7 @@ async function ensureEASUpdatesIsConfiguredNativelyAsync(
* - Sets `updates.url` if not set
* - Ensure latest changes are reflected in the native config, if any
*/
export async function ensureEASUpdatesIsConfiguredAsync(
export async function ensureEASUpdateIsConfiguredAsync(
graphqlClient: ExpoGraphqlClient,
{
exp: expWithoutUpdates,
@@ -300,8 +363,10 @@ export async function ensureEASUpdatesIsConfiguredAsync(
workflows,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
if (projectChanged || !hasExpoUpdates) {
await ensureEASUpdatesIsConfiguredNativelyAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredNativelyAsync(graphqlClient, {
exp: expWithUpdates,
projectDir,
projectId,
@@ -0,0 +1,58 @@
import { ensureBranchExistsAsync } from '../branch/queries';
import { ChannelNotFoundError } from '../channel/errors';
import { createChannelOnAppAsync } from '../channel/queries';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { ChannelQuery } from '../graphql/queries/ChannelQuery';
export async function getBranchNameFromChannelNameAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
channelName: string
): Promise<string> {
let branchName;
try {
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName,
});
if (channel.updateBranches.length === 1) {
branchName = channel.updateBranches[0].name;
} else if (channel.updateBranches.length === 0) {
throw new Error(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
);
} else {
throw new Error(
"Channel has multiple branches associated with it. Instead, use 'eas update --branch'"
);
}
} catch (error) {
if (!(error instanceof ChannelNotFoundError)) {
throw error;
}
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName: channelName,
});
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
appId: projectId,
channelName,
branchId,
});
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on project with id ${projectId}`
);
}
branchName = channelName;
}
return branchName;
}
+3 -1
View File
@@ -126,7 +126,9 @@ export async function selectUpdateGroupOnBranchAsync(
}),
promptOptions: {
title: 'Load more update groups?',
createDisplayTextForSelectionPromptListItem: updateGroup => formatUpdateTitle(updateGroup[0]),
createDisplayTextForSelectionPromptListItem: updateGroup => ({
title: formatUpdateTitle(updateGroup[0]),
}),
getIdentifierForQueryItem: updateGroup => updateGroup[0].group,
},
});
@@ -129,7 +129,7 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: item => 'item: ' + item.value,
createDisplayTextForSelectionPromptListItem: item => ({ title: 'item: ' + item.value }),
},
});
@@ -179,7 +179,7 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: item => item.value,
createDisplayTextForSelectionPromptListItem: item => ({ title: item.value }),
},
});
+82 -45
View File
@@ -1,23 +1,28 @@
import spawnAsync from '@expo/spawn-async';
import cliProgress from 'cli-progress';
import glob from 'fast-glob';
import fs from 'fs';
import fs from 'fs-extra';
import path from 'path';
import { Stream } from 'stream';
import { extract } from 'tar';
import { promisify } from 'util';
import { v4 as uuidv4 } from 'uuid';
import fetch, { RequestInit } from '../fetch';
import fetch, { RequestInit, Response } from '../fetch';
import { AppPlatform } from '../graphql/generated';
import Log from '../log';
import { formatBytes } from './files';
import { getTmpDirectory } from './paths';
import { ProgressHandler, createProgressTracker } from './progress';
const pipeline = promisify(Stream.pipeline);
type ProgressCallback = (progress: number, total: number, loaded: number) => void;
function wrapFetchWithProgress() {
return async (url: string, init: RequestInit, onProgressCallback: ProgressCallback) => {
function wrapFetchWithProgress(): (
url: string,
init: RequestInit,
progressHandler: ProgressHandler
) => Promise<Response> {
let didProgressBarFinish = false;
return async (url: string, init: RequestInit, progressHandler: ProgressHandler) => {
const response = await fetch(url, init);
if (response.ok) {
@@ -40,7 +45,15 @@ function wrapFetchWithProgress() {
const progress = length / total;
onProgressCallback(progress, total, length);
if (!didProgressBarFinish) {
progressHandler({
progress: { total, percent: progress, transferred: length },
isComplete: total === length,
});
if (total === length) {
didProgressBarFinish = true;
}
}
};
response.body.on('data', chunk => {
@@ -56,72 +69,96 @@ function wrapFetchWithProgress() {
};
}
async function downloadFileWithProgressBarAsync(
async function downloadFileWithProgressTrackerAsync(
url: string,
outputPath: string,
infoMessage?: string
progressTrackerMessage: string | ((ratio: number, total: number) => string),
progressTrackerCompletedMessage: string
): Promise<void> {
Log.newLine();
Log.log(infoMessage ? infoMessage : `Downloading file from ${url}...`);
const downloadProgressBar = new cliProgress.SingleBar(
{ format: '|{bar}|' },
cliProgress.Presets.rect
);
try {
const response = await wrapFetchWithProgress()(
url,
{
timeout: 1000 * 60 * 5, // 5 minutes
},
createProgressTracker({
message: progressTrackerMessage,
completedMessage: progressTrackerCompletedMessage,
})
);
let downloadProgressBarStarted = false;
const response = await wrapFetchWithProgress()(
url,
{
timeout: 1000 * 60 * 5, // 5 minutes
},
(_progress: number, total: number, loaded: number): void => {
if (!downloadProgressBarStarted) {
downloadProgressBar.start(total, loaded);
downloadProgressBarStarted = true;
} else if (loaded < total) {
downloadProgressBar.update(loaded);
} else {
downloadProgressBar.stop();
}
if (!response.ok) {
throw new Error(`Failed to download file from ${url}`);
}
);
if (!response.ok) {
throw new Error(`Failed to download file from ${url}`);
await pipeline(response.body, fs.createWriteStream(outputPath));
} catch (error: any) {
if (await fs.pathExists(outputPath)) {
await fs.remove(outputPath);
}
throw error;
}
await pipeline(response.body, fs.createWriteStream(outputPath));
}
export async function downloadAndExtractAppAsync(
async function maybeCacheAppAsync(appPath: string, cachedAppPath?: string): Promise<string> {
if (cachedAppPath) {
await fs.ensureDir(path.dirname(cachedAppPath));
await fs.move(appPath, cachedAppPath);
return cachedAppPath;
}
return appPath;
}
export async function downloadAndMaybeExtractAppAsync(
url: string,
applicationExtension: string
platform: AppPlatform,
cachedAppPath?: string
): Promise<string> {
const outputDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(outputDir, { recursive: true });
const tmpArchivePathDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(tmpArchivePathDir, { recursive: true });
if (url.endsWith('apk')) {
const apkFilePath = path.join(outputDir, `${uuidv4()}.apk`);
await downloadFileWithProgressTrackerAsync(
url,
apkFilePath,
(ratio, total) => `Downloading app (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
'Successfully downloaded app'
);
return maybeCacheAppAsync(apkFilePath, cachedAppPath);
} else {
const tmpArchivePathDir = path.join(getTmpDirectory(), uuidv4());
await fs.mkdir(tmpArchivePathDir, { recursive: true });
const tmpArchivePath = path.join(tmpArchivePathDir, `${uuidv4()}.tar.gz`);
const tmpArchivePath = path.join(tmpArchivePathDir, `${uuidv4()}.tar.gz`);
await downloadFileWithProgressBarAsync(url, tmpArchivePath, 'Downloading app archive...');
await tarExtractAsync(tmpArchivePath, outputDir);
await downloadFileWithProgressTrackerAsync(
url,
tmpArchivePath,
(ratio, total) =>
`Downloading app archive (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
'Successfully downloaded app archive'
);
await tarExtractAsync(tmpArchivePath, outputDir);
return await getAppPathAsync(outputDir, applicationExtension);
const appPath = await getAppPathAsync(outputDir, platform === AppPlatform.Ios ? 'app' : 'apk');
return maybeCacheAppAsync(appPath, cachedAppPath);
}
}
export async function extractAppFromLocalArchiveAsync(
appArchivePath: string,
applicationExtension: string
platform: AppPlatform
): Promise<string> {
const outputDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(outputDir, { recursive: true });
await tarExtractAsync(appArchivePath, outputDir);
return await getAppPathAsync(outputDir, applicationExtension);
return await getAppPathAsync(outputDir, platform === AppPlatform.Android ? 'apk' : 'app');
}
async function getAppPathAsync(outputDir: string, applicationExtension: string): Promise<string> {
+9 -15
View File
@@ -23,7 +23,10 @@ function dotExpoHomeDirectory(): string {
return dirPath;
}
const getStateJsonPath = (): string => path.join(dotExpoHomeDirectory(), 'state.json');
export const getStateJsonPath = (): string => path.join(dotExpoHomeDirectory(), 'state.json');
export const getEasBuildRunCacheDirectoryPath = (): string =>
path.join(getTmpDirectory(), 'eas-build-run-cache');
// Paths for storing things like data, config, cache, etc.
// Should use the correct OS-specific paths (e.g. XDG base directory on Linux)
@@ -35,17 +38,8 @@ const {
temp: TEMP_PATH,
} = envPaths('eas-cli');
const getDataDirectory = (): string => DATA_PATH;
const getConfigDirectory = (): string => CONFIG_PATH;
const getCacheDirectory = (): string => CACHE_PATH;
const getLogDirectory = (): string => LOG_PATH;
const getTmpDirectory = (): string => TEMP_PATH;
export {
getDataDirectory,
getConfigDirectory,
getCacheDirectory,
getLogDirectory,
getTmpDirectory,
getStateJsonPath,
};
export const getDataDirectory = (): string => DATA_PATH;
export const getConfigDirectory = (): string => CONFIG_PATH;
export const getCacheDirectory = (): string => CACHE_PATH;
export const getLogDirectory = (): string => LOG_PATH;
export const getTmpDirectory = (): string => TEMP_PATH;
+7 -5
View File
@@ -21,7 +21,7 @@ export function createProgressTracker({
completedMessage,
}: {
total?: number;
message: string | ((ratio: number) => string);
message: string | ((ratio: number, total: number) => string);
completedMessage: string | ((duration: string) => string);
}): ProgressHandler {
let bar: Ora | null = null;
@@ -31,17 +31,19 @@ export function createProgressTracker({
const timerLabel = String(Date.now());
const getMessage = (v: number): string => {
const getMessage = (v: number, total: number): string => {
const ratio = Math.min(Math.max(v, 0), 1);
const percent = Math.floor(ratio * 100);
return typeof message === 'string' ? `${message} ${percent.toFixed(0)}%` : message(ratio);
return typeof message === 'string'
? `${message} ${percent.toFixed(0)}%`
: message(ratio, total);
};
return ({ progress, isComplete, error }) => {
if (progress) {
if (!bar && (progress.total !== undefined || total !== undefined)) {
calcTotal = (total ?? progress.total) as number;
bar = ora(getMessage(0)).start();
bar = ora(getMessage(0, calcTotal)).start();
startTimer(timerLabel);
}
if (progress.total) {
@@ -56,7 +58,7 @@ export function createProgressTracker({
percentage = current / calcTotal;
}
bar.text = getMessage(percentage);
bar.text = getMessage(percentage, calcTotal);
}
transferredSoFar = progress.transferred;
}
+9 -2
View File
@@ -3,6 +3,11 @@ import uniqBy from './expodash/uniqBy';
const fetchMoreValue = '_fetchMore';
export interface SelectPromptEntry {
title: string;
description?: string;
}
type BasePaginatedQueryArgs<QueryReturnType extends Record<string, any>> = {
limit: number;
offset: number;
@@ -21,7 +26,9 @@ type PaginatedQueryWithSelectPromptArgs<QueryReturnType extends Record<string, a
BasePaginatedQueryArgs<QueryReturnType> & {
promptOptions: {
readonly title: string;
createDisplayTextForSelectionPromptListItem: (queryItem: QueryReturnType) => string;
createDisplayTextForSelectionPromptListItem: (
queryItem: QueryReturnType
) => SelectPromptEntry;
getIdentifierForQueryItem: (queryItem: QueryReturnType) => string;
};
};
@@ -100,7 +107,7 @@ async function paginatedQueryWithSelectPromptInternalAsync<
const selectionPromptListItems = uniqBy(newAccumulator, queryItem =>
promptOptions.getIdentifierForQueryItem(queryItem)
).map(queryItem => ({
title: promptOptions.createDisplayTextForSelectionPromptListItem(queryItem),
...promptOptions.createDisplayTextForSelectionPromptListItem(queryItem),
value: promptOptions.getIdentifierForQueryItem(queryItem),
}));
if (areMorePagesAvailable) {
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "2.7.1",
"version": "3.0.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
@@ -14,6 +14,7 @@
"joi": "17.6.4",
"log-symbols": "4.1.0",
"semver": "7.3.8",
"terminal-link": "2.1.1",
"tslib": "2.4.0"
},
"devDependencies": {
+32
View File
@@ -3,12 +3,40 @@ import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import * as fleece from 'golden-fleece';
import { ValidationError } from 'joi';
import path from 'path';
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
import { link } from './log';
import { EasJsonSchema } from './schema';
import { EasJson } from './types';
const customErrorMessageHandlers: ((err: ValidationError) => void)[] = [
// Ask user to upgrade eas-cli version or check the docs when image is invalid.
(err: ValidationError) => {
for (const detail of err.details) {
// image should be only placed under 'build.profilename.platform.image' key
// if it's not the case show standard Joi error
if (
detail.path.length === 4 &&
detail.path[0] === 'build' &&
['ios', 'android'].includes(detail.path[2].toString()) &&
detail.path[3] === 'image'
) {
throw new InvalidEasJsonError(
chalk.red(
`Specified build image '${
detail?.context?.value
}' is not recognized. Please update your EAS CLI and see ${link(
'https://docs.expo.dev/build-reference/infrastructure/'
)} for supported build images.`
)
);
}
}
},
];
export class EasJsonAccessor {
private easJsonPath: string;
@@ -39,6 +67,10 @@ export class EasJsonAccessor {
noDefaults: true,
});
if (error) {
for (const handler of customErrorMessageHandlers) {
handler(error);
}
const errorMessages = error.message.split('. ');
throw new InvalidEasJsonError(
`${chalk.bold('eas.json')} is not valid.\n- ${errorMessages.join('\n- ')}`
+16
View File
@@ -0,0 +1,16 @@
import chalk from 'chalk';
import terminalLink from 'terminal-link';
// link function from packages/eas-cli/src/log.ts
export function link(
url: string,
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
): string {
let output: string;
if (terminalLink.isSupported) {
output = terminalLink(text, url);
} else {
output = `${text === url ? '' : text + ': '}${chalk.underline(url)}`;
}
return dim ? chalk.dim(output) : output;
}