mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64d985fcda | ||
|
|
bd2bb4dcf6 | ||
|
|
17db6c4cf1 | ||
|
|
5203096aca | ||
|
|
a53e1f916f | ||
|
|
09a4b3d387 | ||
|
|
24e7de5bfb | ||
|
|
613fb5b6ea | ||
|
|
4e117075fa | ||
|
|
33f4ae7a05 | ||
|
|
caba28901d | ||
|
|
77e02f5759 | ||
|
|
fa2717aada | ||
|
|
322f797cbe | ||
|
|
2a64b099ed | ||
|
|
81465733e8 | ||
|
|
9b22056205 | ||
|
|
24841f82e0 | ||
|
|
a8fae470d0 | ||
|
|
72283cdda2 | ||
|
|
9874ff26b3 | ||
|
|
85d64b7ad3 | ||
|
|
2dba2705b7 | ||
|
|
d1bec6e00a | ||
|
|
597e012ca3 | ||
|
|
8632b8ada7 | ||
|
|
b288879838 | ||
|
|
8beb8f46da | ||
|
|
9120bf5777 | ||
|
|
01619d4182 | ||
|
|
aa4ea4ae2a | ||
|
|
c236c2ffb5 | ||
|
|
b5a3a513de | ||
|
|
6058183c45 | ||
|
|
a042d27573 | ||
|
|
88b21e476d | ||
|
|
3d497d4f05 | ||
|
|
9b668e3629 | ||
|
|
a670a2a3fc | ||
|
|
53a407fa78 | ||
|
|
5ab1f637a5 | ||
|
|
f59c945347 | ||
|
|
83e50ab546 | ||
|
|
9a415fe743 | ||
|
|
e722b6601c | ||
|
|
170e5145f0 | ||
|
|
405c532f13 | ||
|
|
5bed1c3a01 | ||
|
|
8d0d34c497 | ||
|
|
6270bc122b | ||
|
|
57f1128450 | ||
|
|
cb09906b2e | ||
|
|
fa704c43b3 | ||
|
|
eede0c3dd7 | ||
|
|
1758a4e5e5 |
No files matched your search
@@ -1,2 +1,5 @@
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
**/yarn.lock linguist-generated
|
||||
+91
-5
@@ -8,13 +8,99 @@ 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
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use envs from the build profile to resolve credentials in `eas credentials`. ([#1520](https://github.com/expo/eas-cli/pull/1520) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Print prompt message when failing because of the non-interactive shell. ([#1523](https://github.com/expo/eas-cli/pull/1523) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add SDK version to the analytics build events. ([#1529](https://github.com/expo/eas-cli/pull/1529) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add Xcode 14.1 image to `eas-json`. ([#1511](https://github.com/expo/eas-cli/pull/1511) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [2.7.0](https://github.com/expo/eas-cli/releases/tag/v2.7.0) - 2022-11-11
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support uploading different platform combinations in `eas update`. ([#1461](https://github.com/expo/eas-cli/pull/1461) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add Git commit to EAS Update record. ([#1499](https://github.com/expo/eas-cli/pull/1499) by [@fiberjw](https://github.com/fiberjw))
|
||||
- Use local CLI for `expo export` in SDK +46. ([#1474](https://github.com/expo/eas-cli/pull/1474) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Validate icon PNGs before running Android build. ([#1477](https://github.com/expo/eas-cli/pull/1477) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed the values of the distribution enum on the build profile schema receiving the incorrect descriptions. ([#1504](https://github.com/expo/eas-cli/pull/1504) by [@macksal](https://github.com/macksal))
|
||||
- Remove hard dependency on expo package being installed to init a project. ([#1517](https://github.com/expo/eas-cli/pull/1517) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove friction from the initial EAS update setup. ([#1479](https://github.com/expo/eas-cli/pull/1479) by [@byCedric](https://github.com/byCedric))
|
||||
- Improve choice formatting when selecting a build to submit in the `eas submit` command. ([#1502](https://github.com/expo/eas-cli/pull/1502) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [2.6.0](https://github.com/expo/eas-cli/releases/tag/v2.6.0) - 2022-10-27
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add custom metadata validation command with more complex rules. ([#1416](https://github.com/expo/eas-cli/pull/1416) by [@byCedric](https://github.com/byCedric))
|
||||
- Add `eas build:run` command which runs iOS simulator builds from the CLI. ([#1447](https://github.com/expo/eas-cli/pull/1447) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Add feature gate support. ([#1475](https://github.com/expo/eas-cli/pull/1475) by [@wschurman](https://github.com/wschurman))
|
||||
- Warn about outdated build deployment when configuring EAS Update.
|
||||
([#1467](https://github.com/expo/eas-cli/pull/1467) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
- Warn about outdated build deployment when configuring EAS Update. ([#1467](https://github.com/expo/eas-cli/pull/1467) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -256,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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "2.6.0"
|
||||
"version": "3.1.0"
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"build:tarballs:linux": "yarn build-for-size-check",
|
||||
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --parallel",
|
||||
"start": "lerna run watch --stream --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint .",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -5,6 +5,8 @@ documents:
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
- 'src/branch/**/*.ts'
|
||||
- 'src/channel/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
|
||||
Generated
+715
-56
@@ -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",
|
||||
@@ -17413,6 +17717,35 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "GitHubAppEnvironment",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "DEVELOPMENT",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "PRODUCTION",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "STAGING",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "GitHubAppInstallation",
|
||||
@@ -17551,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
|
||||
@@ -17870,6 +18199,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "environment",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "GitHubAppEnvironment",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "installation",
|
||||
"description": "",
|
||||
@@ -17903,6 +18248,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "searchRepositories",
|
||||
"description": "",
|
||||
@@ -18066,6 +18427,18 @@
|
||||
"name": "GitHubRepositoryMetadata",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "defaultBranch",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "githubRepoDescription",
|
||||
"description": "",
|
||||
@@ -18158,6 +18531,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "openGraphImageUrl",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "private",
|
||||
"description": "",
|
||||
@@ -20665,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": "",
|
||||
@@ -20936,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": "",
|
||||
@@ -20948,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": "",
|
||||
@@ -20984,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": "",
|
||||
@@ -22394,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",
|
||||
@@ -22847,6 +23347,18 @@
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "GCS",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "NONE",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "S3",
|
||||
"description": "",
|
||||
@@ -23176,6 +23688,50 @@
|
||||
"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": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "isGitWorkingTreeDirty",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"description": "",
|
||||
@@ -24995,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",
|
||||
@@ -27512,6 +28143,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "gitCommitHash",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "group",
|
||||
"description": "",
|
||||
@@ -27544,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": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "2.6.0",
|
||||
"version": "3.1.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -13,8 +13,8 @@
|
||||
"@expo/config": "7.0.1",
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.96",
|
||||
"@expo/eas-json": "2.6.0",
|
||||
"@expo/eas-build-job": "0.2.97",
|
||||
"@expo/eas-json": "3.0.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
@@ -66,6 +66,7 @@
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"pngjs": "6.0.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-retry": "2.0.1",
|
||||
"prompts": "2.4.2",
|
||||
@@ -89,11 +90,13 @@
|
||||
"@types/cli-progress": "3.11.0",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/express": "4.17.14",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.3.0",
|
||||
"@types/pngjs": "6.0.1",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.4.1",
|
||||
"@types/semver": "7.3.12",
|
||||
@@ -103,6 +106,7 @@
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.27.2",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.18.2",
|
||||
"memfs": "3.4.7",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.9",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { fixS3Url } from '../uploads';
|
||||
|
||||
describe(fixS3Url, () => {
|
||||
it('fixes broken links', () => {
|
||||
const input =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
const expectedOutput =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
expect(fixS3Url(input)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
it('does not change correct urls', () => {
|
||||
const correctUrl =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
expect(fixS3Url(correctUrl)).toBe(correctUrl);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,25 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { UpdateBranchFragment } from '../graphql/generated';
|
||||
import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranch,
|
||||
UpdateBranchFragment,
|
||||
} from '../graphql/generated';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import Log from '../log';
|
||||
import { formatBranch, getBranchDescription } from '../update/utils';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
SelectPromptEntry,
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import { BranchNotFoundError } from './utils';
|
||||
|
||||
export const BRANCHES_LIMIT = 50;
|
||||
|
||||
@@ -23,7 +32,7 @@ export async function selectBranchOnAppAsync(
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
|
||||
displayTextForListItem: (queryItem: UpdateBranchFragment) => SelectPromptEntry;
|
||||
promptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
@@ -111,3 +120,65 @@ function renderPageOfBranches(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUpdateBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateBranchForApp($appId: ID!, $name: String!) {
|
||||
updateBranch {
|
||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const newBranch = result.updateBranch.createUpdateBranchForApp;
|
||||
if (!newBranch) {
|
||||
throw new Error(`Could not create branch ${name}.`);
|
||||
}
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
export async function ensureBranchExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchName: string;
|
||||
}
|
||||
): Promise<{ branchId: string; createdBranch: boolean }> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
const { id } = updateBranch;
|
||||
return { branchId: id, createdBranch: false };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id, createdBranch: true };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,142 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { ExitError } from '@oclif/core/lib/errors';
|
||||
import path from 'path';
|
||||
import { instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { CommonContext } from '../context';
|
||||
import { validatePNGsForManagedProjectAsync } from '../validate';
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
|
||||
describe(validatePNGsForManagedProjectAsync, () => {
|
||||
it('does not validate PNGs for generic projects', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
||||
const ctx = instance(ctxMock);
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('does not validate PNGs for projects with SDK >= 47', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
||||
sdkVersion: '47.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
describe(Platform.ANDROID, () => {
|
||||
it('exits if foregroundImage is not a file with .png extension', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if foregroundImage is not a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-jpg.png') } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('does not throw if foregroundImage is a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-alpha.png') } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
xdescribe(Platform.IOS, () => {
|
||||
it('exits if icon is not a file with .png extension', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'assets/icon.jpg') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if icon is not a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-jpg.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if icon has alpha channel (transparency)', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-alpha.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('does not throw if icon is a png file and does not have alpha channel', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-no-alpha.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
@@ -53,6 +57,7 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
await validatePNGsForManagedProjectAsync(ctx);
|
||||
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../platform';
|
||||
import { uploadFileAtPathToS3Async } from '../uploads';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
@@ -113,7 +113,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
type: ArchiveSourceType.GCS,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
@@ -219,9 +219,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
|
||||
projectTarballPath = projectTarball.path;
|
||||
|
||||
const { bucketKey } = await uploadFileAtPathToS3Async(
|
||||
const bucketKey = await uploadFileAtPathToGCSAsync(
|
||||
ctx.graphqlClient,
|
||||
UploadSessionType.EasBuildProjectSources,
|
||||
UploadSessionType.EasBuildGcsProjectSources,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
@@ -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,
|
||||
|
||||
@@ -78,6 +78,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
const analyticsEventProperties = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
|
||||
...(accountId && { account_id: accountId }),
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
|
||||
@@ -16,6 +16,11 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
type: ProjectArchiveSourceType.S3,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.GCS) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Gcs,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.URL) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Url,
|
||||
|
||||
@@ -10,7 +10,11 @@ import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/ta
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
@@ -28,6 +32,7 @@ export async function createIosContextAsync(
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
await validatePNGsForManagedProjectAsync(ctx);
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.115';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.120';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Errors } from '@oclif/core';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { isPNGAsync } from '../utils/image';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { CommonContext } from './context';
|
||||
|
||||
@@ -50,3 +53,137 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
function isInsideDirectory(file: string, directory: string): boolean {
|
||||
return file.startsWith(directory);
|
||||
}
|
||||
|
||||
export async function validatePNGsForManagedProjectAsync<T extends Platform>(
|
||||
ctx: CommonContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow !== Workflow.MANAGED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't run PNG checks on SDK 47 and newer
|
||||
// see https://github.com/expo/eas-cli/pull/1477#issuecomment-1293914917
|
||||
if (!ctx.exp.sdkVersion || semver.satisfies(ctx.exp.sdkVersion, '>= 47.0.0')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
await validateAndroidPNGsAsync(ctx as CommonContext<Platform.ANDROID>);
|
||||
}
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
//
|
||||
// else {
|
||||
// await validateIosPNGsAsync(ctx as CommonContext<Platform.IOS>);
|
||||
// }
|
||||
}
|
||||
|
||||
type ConfigPng = { configPath: string; pngPath: string | undefined };
|
||||
|
||||
async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): Promise<void> {
|
||||
const pngs: ConfigPng[] = [
|
||||
{
|
||||
configPath: 'exp.icon',
|
||||
pngPath: ctx.exp.icon,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.icon',
|
||||
pngPath: ctx.exp.android?.icon,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.adaptiveIcon.foregroundImage',
|
||||
pngPath: ctx.exp.android?.adaptiveIcon?.foregroundImage,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.adaptiveIcon.backgroundImage',
|
||||
pngPath: ctx.exp.android?.adaptiveIcon?.backgroundImage,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.splash.image',
|
||||
pngPath: ctx.exp.splash?.image,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.notification.icon',
|
||||
pngPath: ctx.exp.notification?.icon,
|
||||
},
|
||||
];
|
||||
await validatePNGsAsync(pngs);
|
||||
}
|
||||
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
//
|
||||
// async function validateIosPNGsAsync(ctx: CommonContext<Platform.IOS>): Promise<void> {
|
||||
// const pngs: ConfigPng[] = [
|
||||
// {
|
||||
// configPath: 'exp.icon',
|
||||
// pngPath: ctx.exp.icon,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.ios.icon',
|
||||
// pngPath: ctx.exp.ios?.icon,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.splash.image',
|
||||
// pngPath: ctx.exp.splash?.image,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.notification.icon',
|
||||
// pngPath: ctx.exp.notification?.icon,
|
||||
// },
|
||||
// ];
|
||||
// await validatePNGsAsync(pngs);
|
||||
|
||||
// const icon = ctx.exp.ios?.icon ?? ctx.exp.icon;
|
||||
// if (!icon) {
|
||||
// return;
|
||||
// }
|
||||
// const iconConfigPath = `expo${ctx.exp.ios?.icon ? '.ios' : ''}.icon`;
|
||||
|
||||
// try {
|
||||
// await ensurePNGIsNotTransparentAsync(icon);
|
||||
// } catch (err: any) {
|
||||
// if (err instanceof ImageTransparencyError) {
|
||||
// Log.error(
|
||||
// `Your iOS app icon (${iconConfigPath}) can't have transparency if you wish to upload your app to the Apple App Store.`
|
||||
// );
|
||||
// Log.error(learnMore('https://expo.fyi/remove-alpha-channel', { dim: false }));
|
||||
// Errors.exit(1);
|
||||
// } else {
|
||||
// throw err;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
async function validatePNGsAsync(configPngs: ConfigPng[]): Promise<void> {
|
||||
const validationPromises = configPngs.map(configPng => validatePNGAsync(configPng));
|
||||
const validationResults = await Promise.allSettled(validationPromises);
|
||||
const failedValidations = validationResults.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected'
|
||||
);
|
||||
|
||||
if (failedValidations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.error('PNG images validation failed:');
|
||||
for (const { reason } of failedValidations) {
|
||||
const error: Error = reason;
|
||||
Log.error(`- ${error.message}`);
|
||||
}
|
||||
Errors.exit(1);
|
||||
}
|
||||
|
||||
async function validatePNGAsync({ configPath, pngPath }: ConfigPng): Promise<void> {
|
||||
if (!pngPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pngPath.endsWith('.png')) {
|
||||
throw new Error(`"${configPath}" is not a PNG file`);
|
||||
}
|
||||
|
||||
if (!(await isPNGAsync(pngPath))) {
|
||||
throw new Error(`"${configPath}" is not valid PNG`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class ChannelNotFoundError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Channel not found.');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
ViewBranchesOnUpdateChannelQueryVariables,
|
||||
ViewUpdateChannelsOnAppQueryVariables,
|
||||
} from '../graphql/generated';
|
||||
@@ -42,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,
|
||||
},
|
||||
});
|
||||
@@ -197,3 +201,64 @@ function renderChannelHeaderContent({
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
}
|
||||
|
||||
export async function createChannelOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}
|
||||
): Promise<CreateUpdateChannelOnAppMutation> {
|
||||
// Point the new channel at a branch with its same name.
|
||||
const branchMapping = JSON.stringify({
|
||||
data: [{ branchId, branchMappingLogic: 'true' }],
|
||||
version: 0,
|
||||
});
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
|
||||
updateChannel {
|
||||
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
|
||||
id
|
||||
name
|
||||
branchMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name: channelName,
|
||||
branchMapping,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureChannelExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, branchId, channelName }: { appId: string; branchId: string; channelName: string }
|
||||
): Promise<void> {
|
||||
try {
|
||||
await createChannelOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -158,9 +158,13 @@ describe(getProjectIdAsync, () => {
|
||||
expect(projectId).toEqual('2345');
|
||||
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
});
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith(
|
||||
'/app',
|
||||
{
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if writing the ID back to the config fails', async () => {
|
||||
|
||||
@@ -25,9 +25,13 @@ export async function saveProjectIdToAppConfigAsync(
|
||||
options: { env?: Env } = {}
|
||||
): Promise<void> {
|
||||
const exp = getExpoConfig(projectDir, options);
|
||||
const result = await modifyConfigAsync(projectDir, {
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
});
|
||||
const result = await modifyConfigAsync(
|
||||
projectDir,
|
||||
{
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
|
||||
@@ -1,52 +1,14 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export async function createUpdateBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
gql`
|
||||
mutation createUpdateBranchForApp($appId: ID!, $name: String!) {
|
||||
updateBranch {
|
||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const newBranch = result.updateBranch.createUpdateBranchForApp;
|
||||
if (!newBranch) {
|
||||
throw new Error(`Could not create branch ${name}.`);
|
||||
}
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
export default class BranchCreate extends EasCommand {
|
||||
static override description = 'create a branch';
|
||||
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,63 +1,16 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
|
||||
import { BranchNotFoundError } from '../../branch/utils';
|
||||
import { createChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
|
||||
export async function createUpdateChannelOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
}: {
|
||||
appId: string;
|
||||
channelName: string;
|
||||
branchId: string;
|
||||
}
|
||||
): Promise<CreateUpdateChannelOnAppMutation> {
|
||||
// Point the new channel at a branch with its same name.
|
||||
const branchMapping = JSON.stringify({
|
||||
data: [{ branchId, branchMappingLogic: 'true' }],
|
||||
version: 0,
|
||||
});
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
|
||||
updateChannel {
|
||||
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
|
||||
id
|
||||
name
|
||||
branchMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name: channelName,
|
||||
branchMapping,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class ChannelCreate extends EasCommand {
|
||||
static override description = 'create a channel';
|
||||
@@ -132,7 +85,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
|
||||
const {
|
||||
updateChannel: { createUpdateChannelForApp: newChannel },
|
||||
} = await createUpdateChannelOnAppAsync(graphqlClient, {
|
||||
} = await createChannelOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName,
|
||||
branchId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,7 @@ export default class Credentials extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.OptionalProjectConfig,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
@@ -21,6 +22,7 @@ export default class Credentials extends EasCommand {
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectConfig,
|
||||
getDynamicProjectConfigAsync,
|
||||
analytics,
|
||||
} = await this.getContextAsync(Credentials, {
|
||||
nonInteractive: false,
|
||||
@@ -31,6 +33,7 @@ export default class Credentials extends EasCommand {
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectConfig ?? null,
|
||||
getDynamicProjectConfigAsync,
|
||||
flags.platform
|
||||
).runAsync();
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export default class Submit extends EasCommand {
|
||||
exclusive: ['latest', 'id', 'path'],
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
description: 'Always print logs from Submission Service',
|
||||
description: 'Always print logs from EAS Submit',
|
||||
default: false,
|
||||
}),
|
||||
wait: Flags.boolean({
|
||||
|
||||
@@ -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,24 +1,11 @@
|
||||
import { ExpoConfig, modifyConfigAsync } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
|
||||
import {
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
|
||||
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
|
||||
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
|
||||
export default class UpdateConfigure extends EasCommand {
|
||||
static override description = 'configure the project to support EAS Update';
|
||||
@@ -30,6 +17,7 @@ export default class UpdateConfigure extends EasCommand {
|
||||
options: ['android', 'ios', 'all'],
|
||||
default: 'all',
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -38,285 +26,44 @@ export default class UpdateConfigure extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
'💡 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.'
|
||||
);
|
||||
const { flags } = await this.parse(UpdateConfigure);
|
||||
const platform = flags.platform as RequestedPlatform;
|
||||
const {
|
||||
projectConfig: { projectId, exp, projectDir },
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(UpdateConfigure, {
|
||||
nonInteractive: true,
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
const platform = flags.platform as RequestedPlatform;
|
||||
|
||||
if (!isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion)) {
|
||||
await installExpoUpdatesAsync(projectDir);
|
||||
}
|
||||
|
||||
const [androidWorkflow, iosWorkflow] = await Promise.all([
|
||||
resolveWorkflowAsync(projectDir, Platform.ANDROID),
|
||||
resolveWorkflowAsync(projectDir, Platform.IOS),
|
||||
]);
|
||||
|
||||
const updatedExp = await configureAppJSONForEASUpdateAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
platform,
|
||||
workflows: {
|
||||
android: androidWorkflow,
|
||||
ios: iosWorkflow,
|
||||
},
|
||||
projectId,
|
||||
});
|
||||
Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`);
|
||||
|
||||
// configure native files for EAS Update
|
||||
if (
|
||||
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
|
||||
androidWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId);
|
||||
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
|
||||
}
|
||||
if (
|
||||
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
|
||||
iosWorkflow === Workflow.GENERIC
|
||||
) {
|
||||
await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId);
|
||||
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`All builds of your app going forward will be eligible to receive updates published with EAS Update.`
|
||||
Log.log(
|
||||
'💡 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 ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
exp,
|
||||
projectId,
|
||||
projectDir,
|
||||
platform,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Your app is configured to run EAS Update!`);
|
||||
}
|
||||
}
|
||||
|
||||
async function configureAppJSONForEASUpdateAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
platform,
|
||||
workflows,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
platform: RequestedPlatform;
|
||||
workflows: {
|
||||
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
|
||||
};
|
||||
projectId: string;
|
||||
}): Promise<ExpoConfig> {
|
||||
// this command is non-interactive in the way it was designed
|
||||
const easUpdateURL = getEASUpdateURL(projectId);
|
||||
const updates = { ...exp.updates, url: easUpdateURL };
|
||||
|
||||
const androidDefaultRuntimeVersion =
|
||||
workflows['android'] === Workflow.GENERIC
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
const iosDefaultRuntimeVersion =
|
||||
workflows['ios'] === Workflow.GENERIC
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
|
||||
const newAndroidRuntimeVersion =
|
||||
exp.android?.runtimeVersion ?? exp.runtimeVersion ?? androidDefaultRuntimeVersion;
|
||||
const newIosRuntimeVersion =
|
||||
exp.ios?.runtimeVersion ?? exp.runtimeVersion ?? iosDefaultRuntimeVersion;
|
||||
|
||||
let newConfig: Partial<ExpoConfig>;
|
||||
let newConfigOnlyAddedValues: Partial<ExpoConfig>;
|
||||
switch (platform) {
|
||||
case RequestedPlatform.All: {
|
||||
if (isRuntimeEqual(newAndroidRuntimeVersion, newIosRuntimeVersion)) {
|
||||
newConfig = {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: undefined,
|
||||
},
|
||||
ios: { ...exp.ios, runtimeVersion: undefined },
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
...(exp.android && 'runtimeVersion' in exp.android
|
||||
? {
|
||||
android: {
|
||||
runtimeVersion: '<remove this key>',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(exp.ios && 'runtimeVersion' in exp.ios
|
||||
? {
|
||||
ios: {
|
||||
runtimeVersion: '<remove this key>',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newConfig = {
|
||||
runtimeVersion: undefined, // top level runtime is redundant if it is specified in both android and ios
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
ios: {
|
||||
...exp.ios,
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
...('runtimeVersion' in exp
|
||||
? {
|
||||
runtimeVersion: '<remove this key>', // top level runtime is redundant if it is specified in both android and ios
|
||||
}
|
||||
: {}),
|
||||
android: {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
ios: {
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RequestedPlatform.Android: {
|
||||
newConfig = {
|
||||
android: {
|
||||
...exp.android,
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
android: {
|
||||
runtimeVersion: newAndroidRuntimeVersion,
|
||||
},
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
case RequestedPlatform.Ios: {
|
||||
newConfig = {
|
||||
ios: {
|
||||
...exp.ios,
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates,
|
||||
};
|
||||
newConfigOnlyAddedValues = {
|
||||
ios: {
|
||||
runtimeVersion: newIosRuntimeVersion,
|
||||
},
|
||||
updates: {
|
||||
url: easUpdateURL,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await modifyConfigAsync(projectDir, newConfig);
|
||||
|
||||
const preexistingAndroidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
|
||||
const preexistingIosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
if (exp.updates?.url) {
|
||||
if (exp.updates.url !== easUpdateURL) {
|
||||
Log.withTick(
|
||||
`Overwrote "${exp.updates?.url}" with "${easUpdateURL}" for the updates.url value in app.json`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
|
||||
}
|
||||
|
||||
if (
|
||||
!preexistingAndroidRuntimeVersion &&
|
||||
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform)
|
||||
) {
|
||||
Log.withTick(
|
||||
`Set ${appPlatformDisplayNames[AppPlatform.Android]} runtimeVersion to "${JSON.stringify(
|
||||
newConfig.android?.runtimeVersion ?? newConfig.runtimeVersion
|
||||
)}" in app.json`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!preexistingIosRuntimeVersion &&
|
||||
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)
|
||||
) {
|
||||
Log.withTick(
|
||||
`Set ${appPlatformDisplayNames[AppPlatform.Ios]} runtimeVersion to "${JSON.stringify(
|
||||
newConfig.ios?.runtimeVersion ?? newConfig.runtimeVersion
|
||||
)}" in app.json`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`It looks like you are using a dynamic configuration! ${learnMore(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:\n${learnMore(
|
||||
'https://expo.fyi/eas-update-config.md'
|
||||
)}\n`
|
||||
);
|
||||
Log.log(chalk.bold(JSON.stringify(newConfigOnlyAddedValues, null, 2)));
|
||||
Log.addNewLineIfNone();
|
||||
if (workflows['android'] === Workflow.GENERIC || workflows['ios'] === Workflow.GENERIC) {
|
||||
Log.warn(
|
||||
`You will also have to manually edit the projects ${chalk.bold(
|
||||
'Expo.plist/AndroidManifest.xml'
|
||||
)}. ${learnMore('https://expo.fyi/eas-update-config.md#native-configuration')}`
|
||||
);
|
||||
}
|
||||
Log.addNewLineIfNone();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
assert(result.config, 'A successful result should have a config');
|
||||
|
||||
return result.config.expo;
|
||||
}
|
||||
|
||||
function isRuntimeEqual(
|
||||
runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' },
|
||||
runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' }
|
||||
): boolean {
|
||||
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
|
||||
return runtimeVersionA === runtimeVersionB;
|
||||
} else if (typeof runtimeVersionA === 'object' && typeof runtimeVersionB === 'object') {
|
||||
return runtimeVersionA.policy === runtimeVersionB.policy;
|
||||
} else {
|
||||
return false;
|
||||
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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
@@ -6,123 +6,96 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import { ensureChannelExistsAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
PublishUpdateGroupInput,
|
||||
StatuspageServiceName,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
UpdatePublishMutation,
|
||||
ViewBranchQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
ExpoCLIExportPlatformFlag,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
filterExportedPlatformsByFlag,
|
||||
isUploadedAssetCountAboveWarningThreshold,
|
||||
resolveInputDirectoryAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
|
||||
import { formatUpdateMessage } from '../../update/utils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
|
||||
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getManifestBodyAsync,
|
||||
signManifestBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
export type PublishPlatformFlag = PublishPlatform | 'all';
|
||||
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
|
||||
|
||||
async function ensureChannelExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
try {
|
||||
await createUpdateChannelOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
Log.withTick(
|
||||
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
|
||||
);
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
channel?: string;
|
||||
message?: string;
|
||||
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;
|
||||
};
|
||||
|
||||
export async function ensureBranchExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name: branchName }: ViewBranchQueryVariables
|
||||
): Promise<{
|
||||
branchId: string;
|
||||
}> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
type UpdateFlags = {
|
||||
auto: boolean;
|
||||
platform: ExpoCLIExportPlatformFlag;
|
||||
branchName?: string;
|
||||
channelName?: string;
|
||||
updateMessage?: string;
|
||||
inputDir: string;
|
||||
skipBundler: boolean;
|
||||
privateKeyPath?: string;
|
||||
json: boolean;
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
const { id } = updateBranch;
|
||||
await ensureChannelExistsAsync(graphqlClient, { appId, branchId: id, channelName: branchName });
|
||||
return { branchId: id };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId,
|
||||
branchId: newUpdateBranch.id,
|
||||
channelName: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
function getRequestedPlatform(platform: ExpoCLIExportPlatformFlag): RequestedPlatform | null {
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
return RequestedPlatform.Android;
|
||||
case 'ios':
|
||||
return RequestedPlatform.Ios;
|
||||
case 'web':
|
||||
return null;
|
||||
case 'all':
|
||||
return RequestedPlatform.All;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,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({
|
||||
@@ -157,7 +134,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
}),
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
options: [
|
||||
// TODO: Add web when it's fully supported
|
||||
...defaultPublishPlatforms,
|
||||
'all',
|
||||
],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
@@ -179,21 +160,21 @@ export default class UpdatePublish extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(UpdatePublish);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
|
||||
const { flags: rawFlags } = await this.parse(UpdatePublish);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
let {
|
||||
branch: branchName,
|
||||
auto: autoFlag,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
platform,
|
||||
'private-key-path': privateKeyPath,
|
||||
'non-interactive': nonInteractive,
|
||||
platform: platformFlag,
|
||||
channelName,
|
||||
updateMessage,
|
||||
inputDir,
|
||||
skipBundler,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
} = flags;
|
||||
nonInteractive,
|
||||
} = this.sanitizeFlags(rawFlags);
|
||||
let branchName = this.sanitizeFlags(rawFlags).branchName;
|
||||
|
||||
const {
|
||||
getDynamicProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -205,11 +186,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const platformFlag = platform as PublishPlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = republish || !!group;
|
||||
|
||||
const { exp, projectId, projectDir } = await getDynamicProjectConfigAsync({
|
||||
const {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
projectId,
|
||||
projectDir,
|
||||
} = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
@@ -219,51 +200,43 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
platform: getRequestedPlatform(platformFlag),
|
||||
projectDir,
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync();
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
|
||||
if (!hasExpoUpdates && nonInteractive) {
|
||||
Errors.error(
|
||||
`${chalk.bold(
|
||||
'expo-updates'
|
||||
)} must already be installed when executing in non-interactive mode`,
|
||||
{ exit: 1 }
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
|
||||
if (channelName && branchName) {
|
||||
throw new Error(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasExpoUpdates) {
|
||||
const install = await confirmAsync({
|
||||
message: chalk`The module {cyan expo-updates} must be installed to load EAS updates in-app. Install?`,
|
||||
instructions: 'The command will abort unless you agree.',
|
||||
});
|
||||
if (install) {
|
||||
await installExpoUpdatesAsync(projectDir);
|
||||
} else {
|
||||
Errors.error(`Install ${chalk.bold('expo-updates')} and try again.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
if (channelName) {
|
||||
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
await checkEASUpdateURLIsSetAsync(exp, projectId);
|
||||
|
||||
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;
|
||||
@@ -282,167 +255,113 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let oldMessage: string, oldRuntimeVersion: string;
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
if (!updateMessage && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
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 (group) {
|
||||
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
|
||||
groupId: group,
|
||||
});
|
||||
updatesToRepublish = updatesByGroup;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --group when in non-interactive mode');
|
||||
}
|
||||
|
||||
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 "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
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 plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
|
||||
.map(update => update.platform)
|
||||
.join(', ')}`;
|
||||
} else {
|
||||
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
Log.withTick(publicationPlatformMessage);
|
||||
({ updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
for (const update of updatesToRepublishFilteredByPlatform) {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
|
||||
if (!message) {
|
||||
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);
|
||||
}
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: `Republish "${oldMessage!}" - group: ${group}`,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
if (!message && autoFlag) {
|
||||
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
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);
|
||||
}
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
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('Building bundle...');
|
||||
try {
|
||||
await buildBundlesAsync({ projectDir, inputDir });
|
||||
bundleSpinner.succeed('Built bundle!');
|
||||
} catch (e) {
|
||||
bundleSpinner.fail('Failed to build bundle!');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const assetSpinner = ora().start('Uploading assets...');
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
const bundleSpinner = ora().start('Exporting...');
|
||||
try {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
|
||||
const uploadResults = await uploadAssetsAsync(
|
||||
graphqlClient,
|
||||
assets,
|
||||
projectId,
|
||||
(totalAssets, missingAssets) => {
|
||||
assetSpinner.text = `Uploading assets. Finished (${
|
||||
totalAssets - missingAssets
|
||||
}/${totalAssets})`;
|
||||
}
|
||||
);
|
||||
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
|
||||
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
const uploadAssetSuccessMessage = uploadedAssetCount
|
||||
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
|
||||
: `Uploading assets skipped -- no new assets found!`;
|
||||
assetSpinner.succeed(uploadAssetSuccessMessage);
|
||||
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
|
||||
bundleSpinner.succeed('Exported bundle(s)');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
bundleSpinner.fail('Export failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const truncatedMessage = truncatePublishUpdateMessage(message!);
|
||||
// After possibly bundling, assert that the input directory can be found.
|
||||
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
|
||||
|
||||
const runtimeToPlatformMapping: Record<string, string[]> = {};
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
|
||||
.filter(pair => pair[1] === runtime)
|
||||
.map(pair => pair[0]);
|
||||
const assetSpinner = ora().start('Uploading...');
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
|
||||
if (truncatedMessage !== updateMessage) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
|
||||
|
||||
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
|
||||
for (const runtime of runtimeVersions) {
|
||||
const platforms = runtimeVersions
|
||||
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
|
||||
.map(({ platform }) => platform);
|
||||
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
|
||||
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
|
||||
}
|
||||
}
|
||||
|
||||
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
branchName,
|
||||
});
|
||||
if (createdBranch) {
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchId,
|
||||
channelName: branchName,
|
||||
});
|
||||
}
|
||||
|
||||
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
|
||||
|
||||
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[] = Object.entries(runtimeToPlatformMapping).map(
|
||||
([runtime, platforms]) => {
|
||||
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
|
||||
({ runtimeVersion, platforms }) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
@@ -450,16 +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 : runtime,
|
||||
runtimeVersion,
|
||||
message: truncatedMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
@@ -528,12 +444,15 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
|
||||
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
|
||||
const newUpdatesForRuntimeVersion = newUpdates.filter(
|
||||
update => update.runtimeVersion === runtime
|
||||
update => update.runtimeVersion === runtime.runtimeVersion
|
||||
);
|
||||
if (newUpdatesForRuntimeVersion.length === 0) {
|
||||
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
|
||||
throw new Error(
|
||||
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
|
||||
);
|
||||
}
|
||||
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
|
||||
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
|
||||
@@ -550,7 +469,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: runtime },
|
||||
{ label: 'Runtime version', value: runtime.runtimeVersion },
|
||||
{ label: 'Platform', value: platforms.join(', ') },
|
||||
{ label: 'Update group ID', value: updateGroupId },
|
||||
...(newAndroidUpdate
|
||||
@@ -558,6 +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}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Website link', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
@@ -577,19 +504,64 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
|
||||
if (nonInteractive && !auto && !(branchName && channelName && updateMessage)) {
|
||||
Errors.error(
|
||||
'--auto or both --channel or --branch and --message are 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,
|
||||
inputDir: flags['input-dir'],
|
||||
skipBundler: flags['skip-bundler'],
|
||||
platform: flags.platform as RequestedPlatform,
|
||||
privateKeyPath: flags['private-key-path'],
|
||||
nonInteractive,
|
||||
json: flags.json ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: PublishPlatformFlag,
|
||||
platforms: PublishPlatform[],
|
||||
projectDir: string
|
||||
): Promise<Record<string, string>> {
|
||||
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
|
||||
|
||||
): Promise<{ platform: string; runtimeVersion: string }[]> {
|
||||
for (const platform of platforms) {
|
||||
if (platform === 'web') {
|
||||
continue;
|
||||
}
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
|
||||
const isManaged =
|
||||
(await resolveWorkflowAsync(projectDir, platform as Platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error(
|
||||
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
|
||||
@@ -598,34 +570,18 @@ async function getRuntimeVersionObjectAsync(
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
return [...new Set(platforms)].map(platform => {
|
||||
if (platform === 'web') {
|
||||
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
|
||||
}
|
||||
return {
|
||||
platform,
|
||||
nullthrows(
|
||||
runtimeVersion: nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${platform}`
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
])
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig, projectId: string): Promise<void> {
|
||||
const configuredURL = exp.updates?.url;
|
||||
const expectedURL = getEASUpdateURL(projectId);
|
||||
|
||||
if (configuredURL !== expectedURL) {
|
||||
throw new Error(
|
||||
`The update URL is incorrectly configured for EAS Update. Set updates.url to ${expectedURL} in your ${chalk.bold(
|
||||
'app.json'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const truncatePublishUpdateMessage = (originalMessage: string): string => {
|
||||
if (originalMessage.length > 1024) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
return originalMessage.substring(0, 1021) + '...';
|
||||
}
|
||||
return originalMessage;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log from '../../log';
|
||||
import { pressAnyKeyToContinueAsync } from '../../prompts';
|
||||
@@ -10,6 +11,7 @@ export interface Action<T = void> {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
projectInfo: CredentialsContextProjectInfo | null;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
runAsync(ctx: CredentialsContext): Promise<T>;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
displayAndroidAppCredentials,
|
||||
displayEmptyAndroidCredentials,
|
||||
} from '../android/utils/printCredentials';
|
||||
import { CredentialsContext } from '../context';
|
||||
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
|
||||
import { ActionInfo, AndroidActionType, Scope } from './Actions';
|
||||
import {
|
||||
buildCredentialsActions,
|
||||
@@ -52,9 +52,16 @@ export class ManageAndroid {
|
||||
const buildProfile = hasProjectContext
|
||||
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
|
||||
: null;
|
||||
let projectInfo: CredentialsContextProjectInfo | null = null;
|
||||
if (hasProjectContext) {
|
||||
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
|
||||
env: buildProfile?.env,
|
||||
});
|
||||
projectInfo = { exp, projectId };
|
||||
}
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
projectInfo: this.callingAction.projectInfo,
|
||||
projectInfo,
|
||||
user: this.callingAction.actor,
|
||||
graphqlClient: this.callingAction.graphqlClient,
|
||||
analytics: this.callingAction.analytics,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { ensureActorHasPrimaryAccount } from '../../user/actions';
|
||||
import { CredentialsContext } from '../context';
|
||||
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
selectAscApiKeysFromAccountAsync,
|
||||
@@ -61,9 +61,18 @@ export class ManageIos {
|
||||
const buildProfile = this.callingAction.projectInfo
|
||||
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
|
||||
: null;
|
||||
|
||||
let projectInfo: CredentialsContextProjectInfo | null = null;
|
||||
if (this.callingAction.projectInfo) {
|
||||
const { exp, projectId } = await this.callingAction.getDynamicProjectConfigAsync({
|
||||
env: buildProfile?.env,
|
||||
});
|
||||
projectInfo = { exp, projectId };
|
||||
}
|
||||
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
projectInfo: this.callingAction.projectInfo,
|
||||
projectInfo,
|
||||
user: this.callingAction.actor,
|
||||
graphqlClient: this.callingAction.graphqlClient,
|
||||
analytics: this.callingAction.analytics,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { selectPlatformAsync } from '../../platform';
|
||||
import { Actor } from '../../user/User';
|
||||
@@ -12,6 +13,7 @@ export class SelectPlatform {
|
||||
public readonly graphqlClient: ExpoGraphqlClient,
|
||||
public readonly analytics: Analytics,
|
||||
public readonly projectInfo: CredentialsContextProjectInfo | null,
|
||||
public readonly getDynamicProjectConfigAsync: DynamicConfigContextFn,
|
||||
private readonly flagPlatform?: string
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+130
-30
@@ -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']>;
|
||||
@@ -2523,6 +2576,12 @@ export type GetSignedAssetUploadSpecificationsResult = {
|
||||
specifications: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum GitHubAppEnvironment {
|
||||
Development = 'DEVELOPMENT',
|
||||
Production = 'PRODUCTION',
|
||||
Staging = 'STAGING'
|
||||
}
|
||||
|
||||
export type GitHubAppInstallation = {
|
||||
__typename?: 'GitHubAppInstallation';
|
||||
accessibleRepositories: GitHubRepositoryPaginationResult;
|
||||
@@ -2541,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'];
|
||||
@@ -2586,7 +2645,9 @@ export type GitHubAppQuery = {
|
||||
__typename?: 'GitHubAppQuery';
|
||||
appIdentifier: Scalars['String'];
|
||||
clientIdentifier: Scalars['String'];
|
||||
environment: GitHubAppEnvironment;
|
||||
installation: GitHubAppInstallation;
|
||||
name: Scalars['String'];
|
||||
searchRepositories: GitHubRepositoryPaginationResult;
|
||||
};
|
||||
|
||||
@@ -2613,12 +2674,14 @@ export type GitHubRepository = {
|
||||
|
||||
export type GitHubRepositoryMetadata = {
|
||||
__typename?: 'GitHubRepositoryMetadata';
|
||||
defaultBranch?: Maybe<Scalars['String']>;
|
||||
githubRepoDescription?: Maybe<Scalars['String']>;
|
||||
githubRepoName: Scalars['String'];
|
||||
githubRepoOwnerName: Scalars['String'];
|
||||
githubRepoUrl: Scalars['String'];
|
||||
lastPushed: Scalars['DateTime'];
|
||||
lastUpdated: Scalars['DateTime'];
|
||||
openGraphImageUrl?: Maybe<Scalars['String']>;
|
||||
private: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
@@ -2972,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']>;
|
||||
@@ -2998,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>;
|
||||
@@ -3239,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']>;
|
||||
@@ -3287,6 +3362,8 @@ export type ProjectArchiveSourceInput = {
|
||||
};
|
||||
|
||||
export enum ProjectArchiveSourceType {
|
||||
Gcs = 'GCS',
|
||||
None = 'NONE',
|
||||
S3 = 'S3',
|
||||
Url = 'URL'
|
||||
}
|
||||
@@ -3336,6 +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;
|
||||
@@ -3618,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'];
|
||||
@@ -3992,8 +4080,10 @@ export type Update = ActivityTimelineProjectActivity & {
|
||||
branchId: Scalars['ID'];
|
||||
codeSigningInfo?: Maybe<CodeSigningInfo>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
gitCommitHash?: Maybe<Scalars['String']>;
|
||||
group: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
isGitWorkingTreeDirty: Scalars['Boolean'];
|
||||
manifestFragment: Scalars['String'];
|
||||
manifestPermalink: Scalars['String'];
|
||||
message?: Maybe<Scalars['String']>;
|
||||
@@ -4540,6 +4630,15 @@ export type CreateUpdateBranchForAppMutationVariables = Exact<{
|
||||
|
||||
export type CreateUpdateBranchForAppMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', createUpdateBranchForApp: { __typename?: 'UpdateBranch', id: string, name: string } } };
|
||||
|
||||
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
branchMapping: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateUpdateChannelOnAppMutation = { __typename?: 'RootMutation', updateChannel: { __typename?: 'UpdateChannelMutation', createUpdateChannelForApp: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } } };
|
||||
|
||||
export type GetBranchInfoQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
@@ -4569,15 +4668,6 @@ export type CancelBuildMutationVariables = Exact<{
|
||||
|
||||
export type CancelBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', cancel: { __typename?: 'Build', id: string, status: BuildStatus } } };
|
||||
|
||||
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
branchMapping: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateUpdateChannelOnAppMutation = { __typename?: 'RootMutation', updateChannel: { __typename?: 'UpdateChannelMutation', createUpdateChannelForApp: { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string } } };
|
||||
|
||||
export type DeleteUpdateChannelMutationVariables = Exact<{
|
||||
channelId: Scalars['ID'];
|
||||
}>;
|
||||
@@ -5004,7 +5094,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __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, 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 CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __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 CreateIosBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
@@ -5014,7 +5104,15 @@ 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, 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 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;
|
||||
@@ -5070,6 +5168,7 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
config: AndroidSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']>;
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -5079,6 +5178,7 @@ export type CreateIosSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
config: IosSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']>;
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -5152,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'];
|
||||
@@ -5162,21 +5262,21 @@ 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'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __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, 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 BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __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 BuildsWithSubmissionsByIdQueryVariables = Exact<{
|
||||
buildId: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __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, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __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 }>, 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 BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __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, submissions: Array<{ __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 }>, 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 ViewBuildsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5186,7 +5286,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __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, 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 ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __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 ViewUpdateChannelOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5194,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'];
|
||||
@@ -5203,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'];
|
||||
@@ -5256,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'];
|
||||
@@ -5267,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'];
|
||||
@@ -5277,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; }>;
|
||||
|
||||
@@ -5303,9 +5403,9 @@ export type AccountFragment = { __typename?: 'Account', id: string, name: string
|
||||
|
||||
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'User', id: string } }> } };
|
||||
|
||||
export type BuildFragment = { __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, 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 BuildFragment = { __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 BuildWithSubmissionsFragment = { __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, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __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 }>, 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 BuildWithSubmissionsFragment = { __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, submissions: Array<{ __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 }>, 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 EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
|
||||
|
||||
@@ -5313,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);
|
||||
},
|
||||
};
|
||||
@@ -28,10 +28,16 @@ export const SubmissionMutation = {
|
||||
$appId: ID!
|
||||
$config: AndroidSubmissionConfigInput!
|
||||
$submittedBuildId: ID
|
||||
$archiveSource: SubmissionArchiveSourceInput
|
||||
) {
|
||||
submission {
|
||||
createAndroidSubmission(
|
||||
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
|
||||
input: {
|
||||
appId: $appId
|
||||
config: $config
|
||||
submittedBuildId: $submittedBuildId
|
||||
archiveSource: $archiveSource
|
||||
}
|
||||
) {
|
||||
submission {
|
||||
id
|
||||
@@ -60,10 +66,16 @@ export const SubmissionMutation = {
|
||||
$appId: ID!
|
||||
$config: IosSubmissionConfigInput!
|
||||
$submittedBuildId: ID
|
||||
$archiveSource: SubmissionArchiveSourceInput
|
||||
) {
|
||||
submission {
|
||||
createIosSubmission(
|
||||
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
|
||||
input: {
|
||||
appId: $appId
|
||||
config: $config
|
||||
submittedBuildId: $submittedBuildId
|
||||
archiveSource: $archiveSource
|
||||
}
|
||||
) {
|
||||
submission {
|
||||
id
|
||||
|
||||
@@ -8,16 +8,17 @@ import {
|
||||
UploadSessionType,
|
||||
} from '../generated';
|
||||
|
||||
export interface PresignedPost {
|
||||
export interface SignedUrl {
|
||||
url: string;
|
||||
fields: Record<string, string>;
|
||||
headers: Record<string, string>;
|
||||
bucketKey: string;
|
||||
}
|
||||
|
||||
export const UploadSessionMutation = {
|
||||
async createUploadSessionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
type: UploadSessionType
|
||||
): Promise<PresignedPost> {
|
||||
): Promise<SignedUrl> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUploadSessionMutation, CreateUploadSessionMutationVariables>(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,6 +45,7 @@ export const BuildFragmentNode = gql`
|
||||
appBuildVersion
|
||||
runtimeVersion
|
||||
gitCommitHash
|
||||
gitCommitMessage
|
||||
initialQueuePosition
|
||||
queuePosition
|
||||
estimatedWaitTimeLeftSeconds
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -45,6 +45,10 @@ export default class Log {
|
||||
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
|
||||
}
|
||||
|
||||
public static fail(message: string): void {
|
||||
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
|
||||
}
|
||||
|
||||
public static succeed(message: string): void {
|
||||
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
filterExportedPlatformsByFlag,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
getBase64URLEncoding,
|
||||
getStorageKey,
|
||||
@@ -91,6 +92,28 @@ describe('MetadataJoi', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(filterExportedPlatformsByFlag, () => {
|
||||
it(`returns all`, () => {
|
||||
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'all')).toEqual({
|
||||
web: true,
|
||||
ios: true,
|
||||
android: true,
|
||||
});
|
||||
});
|
||||
it(`selects a platform`, () => {
|
||||
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'ios')).toEqual({
|
||||
ios: true,
|
||||
});
|
||||
});
|
||||
it(`asserts selected platform missing`, () => {
|
||||
expect(() =>
|
||||
filterExportedPlatformsByFlag({ web: true }, 'ios')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"--platform="ios" not found in metadata.json. Available platform(s): web"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(guessContentTypeFromExtension, () => {
|
||||
it('returns the correct content type for jpg', () => {
|
||||
expect(guessContentTypeFromExtension('jpg')).toBe('image/jpeg');
|
||||
@@ -236,30 +259,41 @@ describe(resolveInputDirectoryAsync, () => {
|
||||
it('returns the correct distRoot path', async () => {
|
||||
const customDirectoryName = path.resolve(uuidv4());
|
||||
await fs.mkdir(customDirectoryName, { recursive: true });
|
||||
expect(await resolveInputDirectoryAsync(customDirectoryName)).toBe(customDirectoryName);
|
||||
expect(await resolveInputDirectoryAsync(customDirectoryName, { skipBundler: false })).toBe(
|
||||
customDirectoryName
|
||||
);
|
||||
});
|
||||
it('throws an error if the path does not exist', async () => {
|
||||
const nonExistentPath = path.resolve(uuidv4());
|
||||
await expect(resolveInputDirectoryAsync(nonExistentPath)).rejects
|
||||
.toThrow(`The input directory "${nonExistentPath}" does not exist.
|
||||
You can allow us to build it for you by not setting the --skip-bundler flag.
|
||||
If you chose to build it yourself you'll need to run a command to build the JS
|
||||
bundle first.
|
||||
You can use '--input-dir' to specify a different input directory.`);
|
||||
await expect(
|
||||
resolveInputDirectoryAsync(nonExistentPath, { skipBundler: false })
|
||||
).rejects.toThrow(`--input-dir="${nonExistentPath}" not found.`);
|
||||
});
|
||||
it('throws a more specific error if the path does not exist and the dev opted out of bundling', async () => {
|
||||
const nonExistentPath = path.resolve(uuidv4());
|
||||
await expect(
|
||||
resolveInputDirectoryAsync(nonExistentPath, { skipBundler: true })
|
||||
).rejects.toThrow(
|
||||
`--input-dir="${nonExistentPath}" not found. --skip-bundler requires the project to be exported manually before uploading. Ex: npx expo export && eas update --skip-bundler`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(collectAssetsAsync, () => {
|
||||
it('builds an update info group', async () => {
|
||||
const fakeHash = 'md5-hash-of-jpg';
|
||||
const bundles = { android: 'android-bundle-code', ios: 'ios-bundle-code' };
|
||||
const inputDir = uuidv4();
|
||||
const bundles = {
|
||||
android: 'android-bundle-code',
|
||||
ios: 'ios-bundle-code',
|
||||
web: 'web-bundle-code',
|
||||
};
|
||||
const inputDir = path.resolve(uuidv4());
|
||||
|
||||
const userDefinedAssets = [
|
||||
{
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: path.resolve(`${inputDir}/assets/${fakeHash}`),
|
||||
path: `${inputDir}/assets/${fakeHash}`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -285,16 +319,20 @@ describe(collectAssetsAsync, () => {
|
||||
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
|
||||
bundle: 'bundles/ios.js',
|
||||
},
|
||||
web: {
|
||||
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
|
||||
bundle: 'bundles/web.js',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(await collectAssetsAsync({ inputDir, platforms: defaultPublishPlatforms })).toEqual({
|
||||
expect(await collectAssetsAsync(inputDir)).toEqual({
|
||||
android: {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/android.js`),
|
||||
path: `${inputDir}/bundles/android.js`,
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
@@ -302,18 +340,15 @@ describe(collectAssetsAsync, () => {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/ios.js`),
|
||||
path: `${inputDir}/bundles/ios.js`,
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await collectAssetsAsync({ inputDir, platforms: ['ios'] })).toEqual({
|
||||
ios: {
|
||||
web: {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/ios.js`),
|
||||
path: `${inputDir}/bundles/web.js`,
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
import { resolveWorkflowAsync, resolveWorkflowPerPlatformAsync } from '../workflow';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
const projectDir = '/app';
|
||||
|
||||
describe(resolveWorkflowAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
const projectDir = '/app';
|
||||
|
||||
test('bare workflow for both platforms', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
@@ -57,3 +57,24 @@ describe(resolveWorkflowAsync, () => {
|
||||
await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.MANAGED);
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveWorkflowPerPlatformAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
test('returns the correct combined value', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/helloworld.xcodeproj/project.pbxproj': 'fake',
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
},
|
||||
projectDir
|
||||
);
|
||||
|
||||
await expect(resolveWorkflowPerPlatformAsync(projectDir)).resolves.toEqual({
|
||||
android: Workflow.GENERIC,
|
||||
ios: Workflow.GENERIC,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -97,10 +97,19 @@ export async function validateAppVersionRuntimePolicySupportAsync(
|
||||
);
|
||||
}
|
||||
|
||||
export async function installExpoUpdatesAsync(projectDir: string): Promise<void> {
|
||||
export async function installExpoUpdatesAsync(
|
||||
projectDir: string,
|
||||
options?: { silent: boolean }
|
||||
): Promise<void> {
|
||||
Log.log(chalk.gray`> npx expo install expo-updates`);
|
||||
await expoCommandAsync(projectDir, ['install', 'expo-updates']);
|
||||
Log.newLine();
|
||||
try {
|
||||
await expoCommandAsync(projectDir, ['install', 'expo-updates'], { silent: options?.silent });
|
||||
} catch (error: any) {
|
||||
if (options?.silent) {
|
||||
Log.error('stdout' in error ? error.stdout : error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOwnerAccountForProjectIdAsync(
|
||||
|
||||
@@ -10,19 +10,20 @@ import promiseLimit from 'promise-limit';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
|
||||
import { PublishMutation } from '../graphql/mutations/PublishMutation';
|
||||
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
|
||||
import { PublishQuery } from '../graphql/queries/PublishQuery';
|
||||
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
|
||||
import { expoCommandAsync } from '../utils/expoCli';
|
||||
import Log from '../log';
|
||||
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
|
||||
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
|
||||
import chunk from '../utils/expodash/chunk';
|
||||
import uniqBy from '../utils/expodash/uniqBy';
|
||||
|
||||
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
|
||||
export type ExpoCLIExportPlatformFlag = Platform | 'all';
|
||||
|
||||
type Metadata = {
|
||||
version: number;
|
||||
bundler: 'metro';
|
||||
fileMetadata: {
|
||||
[key in 'android' | 'ios']: { assets: { path: string; ext: string }[]; bundle: string };
|
||||
[key in Platform]: { assets: { path: string; ext: string }[]; bundle: string };
|
||||
};
|
||||
};
|
||||
export type RawAsset = {
|
||||
@@ -31,7 +32,7 @@ export type RawAsset = {
|
||||
path: string;
|
||||
};
|
||||
type CollectedAssets = {
|
||||
[platform in PublishPlatform]?: {
|
||||
[platform in Platform]?: {
|
||||
launchAsset: RawAsset;
|
||||
assets: RawAsset[];
|
||||
};
|
||||
@@ -47,7 +48,7 @@ type ManifestFragment = {
|
||||
extra?: ManifestExtra;
|
||||
};
|
||||
type UpdateInfoGroup = {
|
||||
[key in PublishPlatform]: ManifestFragment;
|
||||
[key in Platform]: ManifestFragment;
|
||||
};
|
||||
|
||||
const fileMetadataJoi = Joi.object({
|
||||
@@ -55,13 +56,14 @@ const fileMetadataJoi = Joi.object({
|
||||
.required()
|
||||
.items(Joi.object({ path: Joi.string().required(), ext: Joi.string().required() })),
|
||||
bundle: Joi.string().required(),
|
||||
}).required();
|
||||
}).optional();
|
||||
export const MetadataJoi = Joi.object({
|
||||
version: Joi.number().required(),
|
||||
bundler: Joi.string().required(),
|
||||
fileMetadata: Joi.object({
|
||||
android: fileMetadataJoi,
|
||||
ios: fileMetadataJoi,
|
||||
web: fileMetadataJoi,
|
||||
}).required(),
|
||||
}).required();
|
||||
|
||||
@@ -129,7 +131,7 @@ export async function buildUnsortedUpdateInfoGroupAsync(
|
||||
assets: CollectedAssets,
|
||||
exp: ExpoConfig
|
||||
): Promise<UpdateInfoGroup> {
|
||||
let platform: PublishPlatform;
|
||||
let platform: Platform;
|
||||
const updateInfoGroup: Partial<UpdateInfoGroup> = {};
|
||||
for (platform in assets) {
|
||||
updateInfoGroup[platform] = {
|
||||
@@ -148,33 +150,54 @@ export async function buildUnsortedUpdateInfoGroupAsync(
|
||||
export async function buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp,
|
||||
platformFlag,
|
||||
}: {
|
||||
projectDir: string;
|
||||
inputDir: string;
|
||||
exp: Pick<ExpoConfig, 'sdkVersion'>;
|
||||
platformFlag: ExpoCLIExportPlatformFlag;
|
||||
}): Promise<void> {
|
||||
const packageJSON = JsonFile.read(path.resolve(projectDir, 'package.json'));
|
||||
if (!packageJSON) {
|
||||
throw new Error('Could not locate package.json');
|
||||
}
|
||||
|
||||
await expoCommandAsync(projectDir, [
|
||||
'export',
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--experimental-bundle',
|
||||
'--non-interactive',
|
||||
'--dump-sourcemap',
|
||||
]);
|
||||
if (shouldUseVersionedExpoCLI(projectDir, exp)) {
|
||||
await expoCommandAsync(projectDir, [
|
||||
'export',
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--dump-sourcemap',
|
||||
'--platform',
|
||||
platformFlag,
|
||||
]);
|
||||
} else {
|
||||
// Legacy global Expo CLI
|
||||
await expoCommandAsync(projectDir, [
|
||||
'export',
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--experimental-bundle',
|
||||
'--non-interactive',
|
||||
'--dump-sourcemap',
|
||||
'--platform',
|
||||
platformFlag,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveInputDirectoryAsync(customInputDirectory: string): Promise<string> {
|
||||
const distRoot = path.resolve(customInputDirectory);
|
||||
export async function resolveInputDirectoryAsync(
|
||||
inputDir: string,
|
||||
{ skipBundler }: { skipBundler?: boolean }
|
||||
): Promise<string> {
|
||||
const distRoot = path.resolve(inputDir);
|
||||
if (!(await fs.pathExists(distRoot))) {
|
||||
throw new Error(`The input directory "${customInputDirectory}" does not exist.
|
||||
You can allow us to build it for you by not setting the --skip-bundler flag.
|
||||
If you chose to build it yourself you'll need to run a command to build the JS
|
||||
bundle first.
|
||||
You can use '--input-dir' to specify a different input directory.`);
|
||||
let error = `--input-dir="${inputDir}" not found.`;
|
||||
if (skipBundler) {
|
||||
error += ` --skip-bundler requires the project to be exported manually before uploading. Ex: npx expo export && eas update --skip-bundler`;
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
return distRoot;
|
||||
}
|
||||
@@ -193,43 +216,62 @@ export function loadMetadata(distRoot: string): Metadata {
|
||||
if (metadata.bundler !== 'metro') {
|
||||
throw new Error('Only bundles created with Metro are currently supported');
|
||||
}
|
||||
const platforms = Object.keys(metadata.fileMetadata);
|
||||
if (platforms.length === 0) {
|
||||
Log.warn('No updates were exported for any platform');
|
||||
}
|
||||
Log.debug(`Loaded ${platforms.length} platform(s): ${platforms.join(', ')}`);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function collectAssetsAsync({
|
||||
inputDir,
|
||||
platforms,
|
||||
}: {
|
||||
inputDir: string;
|
||||
platforms: PublishPlatform[];
|
||||
}): Promise<CollectedAssets> {
|
||||
const distRoot = await resolveInputDirectoryAsync(inputDir);
|
||||
const metadata = loadMetadata(distRoot);
|
||||
export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform, any>>>(
|
||||
record: T,
|
||||
platformFlag: ExpoCLIExportPlatformFlag
|
||||
): T {
|
||||
if (platformFlag === 'all') {
|
||||
return record;
|
||||
}
|
||||
|
||||
const assetsFinal: CollectedAssets = {};
|
||||
for (const platform of platforms) {
|
||||
assetsFinal[platform] = {
|
||||
const platform = platformFlag as Platform;
|
||||
|
||||
if (!record[platform]) {
|
||||
throw new Error(
|
||||
`--platform="${platform}" not found in metadata.json. Available platform(s): ${Object.keys(
|
||||
record
|
||||
).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
return { [platform]: record[platform] } as T;
|
||||
}
|
||||
|
||||
/** Given a directory, load the metadata.json and collect the assets for each platform. */
|
||||
export async function collectAssetsAsync(dir: string): Promise<CollectedAssets> {
|
||||
const metadata = loadMetadata(dir);
|
||||
|
||||
const collectedAssets: CollectedAssets = {};
|
||||
|
||||
for (const platform of Object.keys(metadata.fileMetadata) as Platform[]) {
|
||||
collectedAssets[platform] = {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(distRoot, metadata.fileMetadata[platform].bundle),
|
||||
path: path.resolve(dir, metadata.fileMetadata[platform].bundle),
|
||||
},
|
||||
assets: metadata.fileMetadata[platform].assets.map(asset => {
|
||||
let fileExtension;
|
||||
if (asset.ext) {
|
||||
// ensure the file extension has a '.' prefix
|
||||
fileExtension = asset.ext.startsWith('.') ? asset.ext : `.${asset.ext}`;
|
||||
}
|
||||
return {
|
||||
fileExtension,
|
||||
contentType: guessContentTypeFromExtension(asset.ext),
|
||||
path: path.join(distRoot, asset.path),
|
||||
};
|
||||
}),
|
||||
assets: metadata.fileMetadata[platform].assets.map(asset => ({
|
||||
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
|
||||
contentType: guessContentTypeFromExtension(asset.ext),
|
||||
path: path.join(dir, asset.path),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return assetsFinal;
|
||||
return collectedAssets;
|
||||
}
|
||||
|
||||
// ensure the file extension has a '.' prefix
|
||||
function ensureLeadingPeriod(extension: string): string {
|
||||
return extension.startsWith('.') ? extension : `.${extension}`;
|
||||
}
|
||||
|
||||
export async function filterOutAssetsThatAlreadyExistAsync(
|
||||
|
||||
@@ -34,3 +34,13 @@ export async function resolveWorkflowAsync(
|
||||
}
|
||||
return Workflow.MANAGED;
|
||||
}
|
||||
|
||||
export async function resolveWorkflowPerPlatformAsync(
|
||||
projectDir: string
|
||||
): Promise<Record<Platform, Workflow>> {
|
||||
const [android, ios] = await Promise.all([
|
||||
resolveWorkflowAsync(projectDir, Platform.ANDROID),
|
||||
resolveWorkflowAsync(projectDir, Platform.IOS),
|
||||
]);
|
||||
return { android, ios };
|
||||
}
|
||||
@@ -14,7 +14,10 @@ export async function promptAsync<T extends string = string>(
|
||||
options: Options = {}
|
||||
): Promise<Answers<T>> {
|
||||
if (!process.stdin.isTTY && !global.test) {
|
||||
throw new Error('Input is required, but stdin is not readable.');
|
||||
const message = Array.isArray(questions) ? questions[0]?.message : questions.message;
|
||||
throw new Error(
|
||||
`Input is required, but stdin is not readable. Failed to display prompt: ${message}`
|
||||
);
|
||||
}
|
||||
return await prompts<T>(questions, {
|
||||
onCancel() {
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
@@ -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!');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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,
|
||||
})),
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import { URL } from 'url';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AppPlatform, BuildFragment } from '../graphql/generated';
|
||||
import { BuildFragment } from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { appPlatformDisplayNames } from '../platform';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { fromNow } from '../utils/date';
|
||||
import { getRecentBuildsForSubmissionAsync } from './utils/builds';
|
||||
import { isExistingFileAsync, uploadAppArchiveAsync } from './utils/files';
|
||||
|
||||
@@ -21,15 +22,21 @@ export enum ArchiveSourceType {
|
||||
latest,
|
||||
path,
|
||||
buildId,
|
||||
build,
|
||||
buildList,
|
||||
prompt,
|
||||
gcs,
|
||||
}
|
||||
|
||||
export interface ArchiveResolverContext {
|
||||
platform: Platform;
|
||||
projectId: string;
|
||||
nonInteractive: boolean;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}
|
||||
|
||||
interface ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType;
|
||||
platform: Platform;
|
||||
projectId: string;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
interface ArchiveUrlSource extends ArchiveSourceBase {
|
||||
@@ -51,6 +58,11 @@ interface ArchiveBuildIdSource extends ArchiveSourceBase {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ArchiveBuildSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.build;
|
||||
build: BuildFragment;
|
||||
}
|
||||
|
||||
interface ArchiveBuildListSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.buildList;
|
||||
}
|
||||
@@ -59,10 +71,10 @@ interface ArchivePromptSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.prompt;
|
||||
}
|
||||
|
||||
export interface Archive {
|
||||
build?: BuildFragment;
|
||||
source: ArchiveSource;
|
||||
url?: string;
|
||||
interface ArchiveGCSSource extends ArchiveSourceBase {
|
||||
sourceType: ArchiveSourceType.gcs;
|
||||
bucketKey: string;
|
||||
localSource: ArchivePathSource;
|
||||
}
|
||||
|
||||
export type ArchiveSource =
|
||||
@@ -70,54 +82,62 @@ export type ArchiveSource =
|
||||
| ArchiveLatestSource
|
||||
| ArchivePathSource
|
||||
| ArchiveBuildIdSource
|
||||
| ArchiveBuildSource
|
||||
| ArchiveBuildListSource
|
||||
| ArchivePromptSource;
|
||||
| ArchivePromptSource
|
||||
| ArchiveGCSSource;
|
||||
|
||||
export type ResolvedArchiveSource = ArchiveUrlSource | ArchiveGCSSource | ArchiveBuildSource;
|
||||
|
||||
export async function getArchiveAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
ctx: ArchiveResolverContext,
|
||||
source: ArchiveSource
|
||||
): Promise<Archive> {
|
||||
): Promise<ResolvedArchiveSource> {
|
||||
switch (source.sourceType) {
|
||||
case ArchiveSourceType.prompt: {
|
||||
return await handlePromptSourceAsync(graphqlClient, source);
|
||||
return await handlePromptSourceAsync(ctx);
|
||||
}
|
||||
case ArchiveSourceType.url: {
|
||||
return await handleUrlSourceAsync(graphqlClient, source);
|
||||
return await handleUrlSourceAsync(ctx, source);
|
||||
}
|
||||
case ArchiveSourceType.latest: {
|
||||
return await handleLatestSourceAsync(graphqlClient, source);
|
||||
return await handleLatestSourceAsync(ctx);
|
||||
}
|
||||
case ArchiveSourceType.path: {
|
||||
return await handlePathSourceAsync(graphqlClient, source);
|
||||
return await handlePathSourceAsync(ctx, source);
|
||||
}
|
||||
case ArchiveSourceType.buildId: {
|
||||
return await handleBuildIdSourceAsync(graphqlClient, source);
|
||||
return await handleBuildIdSourceAsync(ctx, source);
|
||||
}
|
||||
case ArchiveSourceType.buildList: {
|
||||
return await handleBuildListSourceAsync(graphqlClient, source);
|
||||
return await handleBuildListSourceAsync(ctx);
|
||||
}
|
||||
case ArchiveSourceType.gcs: {
|
||||
return source;
|
||||
}
|
||||
case ArchiveSourceType.build: {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUrlSourceAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
ctx: ArchiveResolverContext,
|
||||
source: ArchiveUrlSource
|
||||
): Promise<Archive> {
|
||||
): Promise<ResolvedArchiveSource> {
|
||||
const { url } = source;
|
||||
|
||||
if (!validateUrl(url)) {
|
||||
Log.error(chalk.bold(`The URL you provided is invalid: ${url}`));
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
const maybeBuildId = isBuildDetailsPage(url);
|
||||
if (maybeBuildId) {
|
||||
if (await askIfUseBuildIdFromUrlAsync(source, maybeBuildId)) {
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
if (await askIfUseBuildIdFromUrlAsync(ctx, source, maybeBuildId)) {
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id: maybeBuildId,
|
||||
});
|
||||
@@ -125,20 +145,19 @@ async function handleUrlSourceAsync(
|
||||
}
|
||||
|
||||
return {
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleLatestSourceAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
source: ArchiveLatestSource
|
||||
): Promise<Archive> {
|
||||
ctx: ArchiveResolverContext
|
||||
): Promise<ResolvedArchiveSource> {
|
||||
try {
|
||||
const [latestBuild] = await getRecentBuildsForSubmissionAsync(
|
||||
graphqlClient,
|
||||
toAppPlatform(source.platform),
|
||||
source.projectId
|
||||
ctx.graphqlClient,
|
||||
toAppPlatform(ctx.platform),
|
||||
ctx.projectId
|
||||
);
|
||||
|
||||
if (!latestBuild) {
|
||||
@@ -147,15 +166,14 @@ async function handleLatestSourceAsync(
|
||||
"Couldn't find any builds for this project on EAS servers. It looks like you haven't run 'eas build' yet."
|
||||
)
|
||||
);
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sourceType: ArchiveSourceType.build,
|
||||
build: latestBuild,
|
||||
source,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
@@ -164,34 +182,37 @@ async function handleLatestSourceAsync(
|
||||
}
|
||||
|
||||
async function handlePathSourceAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
ctx: ArchiveResolverContext,
|
||||
source: ArchivePathSource
|
||||
): Promise<Archive> {
|
||||
): Promise<ResolvedArchiveSource> {
|
||||
if (!(await isExistingFileAsync(source.path))) {
|
||||
Log.error(chalk.bold(`${source.path} doesn't exist`));
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
Log.log('Uploading your app archive to the Expo Submission Service');
|
||||
const uploadUrl = await uploadAppArchiveAsync(graphqlClient, source.path);
|
||||
Log.log('Uploading your app archive to EAS Submit');
|
||||
const bucketKey = await uploadAppArchiveAsync(ctx.graphqlClient, source.path);
|
||||
return {
|
||||
url: uploadUrl,
|
||||
source,
|
||||
sourceType: ArchiveSourceType.gcs,
|
||||
bucketKey,
|
||||
localSource: {
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path: source.path,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function handleBuildIdSourceAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
ctx: ArchiveResolverContext,
|
||||
source: ArchiveBuildIdSource
|
||||
): Promise<Archive> {
|
||||
): Promise<ResolvedArchiveSource> {
|
||||
try {
|
||||
const build = await BuildQuery.byIdAsync(graphqlClient, source.id);
|
||||
const build = await BuildQuery.byIdAsync(ctx.graphqlClient, source.id);
|
||||
|
||||
if (build.platform !== toAppPlatform(source.platform)) {
|
||||
const expectedPlatformName = appPlatformDisplayNames[toAppPlatform(source.platform)];
|
||||
if (build.platform !== toAppPlatform(ctx.platform)) {
|
||||
const expectedPlatformName = appPlatformDisplayNames[toAppPlatform(ctx.platform)];
|
||||
const receivedPlatformName = appPlatformDisplayNames[build.platform];
|
||||
Log.error(
|
||||
chalk.bold(
|
||||
@@ -199,15 +220,14 @@ async function handleBuildIdSourceAsync(
|
||||
)
|
||||
);
|
||||
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sourceType: ArchiveSourceType.build,
|
||||
build,
|
||||
source,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(chalk.bold(`Could not find build with ID ${source.id}`));
|
||||
@@ -219,26 +239,24 @@ async function handleBuildIdSourceAsync(
|
||||
);
|
||||
Log.debug('Original error:', err);
|
||||
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBuildListSourceAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
source: ArchiveBuildListSource
|
||||
): Promise<Archive> {
|
||||
ctx: ArchiveResolverContext
|
||||
): Promise<ResolvedArchiveSource> {
|
||||
try {
|
||||
const appPlatform = toAppPlatform(source.platform);
|
||||
const appPlatform = toAppPlatform(ctx.platform);
|
||||
const expiryDate = new Date(); // artifacts expire after 30 days
|
||||
expiryDate.setDate(expiryDate.getDate() - 30);
|
||||
|
||||
const recentBuilds = await getRecentBuildsForSubmissionAsync(
|
||||
graphqlClient,
|
||||
ctx.graphqlClient,
|
||||
appPlatform,
|
||||
source.projectId,
|
||||
ctx.projectId,
|
||||
{
|
||||
limit: BUILD_LIST_ITEM_COUNT,
|
||||
}
|
||||
@@ -251,8 +269,7 @@ async function handleBuildListSourceAsync(
|
||||
"It looks like you haven't run 'eas build' yet."
|
||||
)
|
||||
);
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
@@ -264,8 +281,7 @@ async function handleBuildListSourceAsync(
|
||||
'EAS keeps your build artifacts only for 30 days.'
|
||||
)
|
||||
);
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
@@ -285,15 +301,14 @@ async function handleBuildListSourceAsync(
|
||||
});
|
||||
|
||||
if (selectedBuild == null) {
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sourceType: ArchiveSourceType.build,
|
||||
build: selectedBuild,
|
||||
source,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
@@ -302,53 +317,39 @@ async function handleBuildListSourceAsync(
|
||||
}
|
||||
|
||||
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
|
||||
const {
|
||||
id,
|
||||
platform,
|
||||
updatedAt,
|
||||
appVersion,
|
||||
sdkVersion,
|
||||
runtimeVersion,
|
||||
buildProfile,
|
||||
appBuildVersion,
|
||||
releaseChannel,
|
||||
initiatingActor,
|
||||
} = build;
|
||||
const { id, updatedAt, runtimeVersion, buildProfile, gitCommitHash, gitCommitMessage, channel } =
|
||||
build;
|
||||
|
||||
const formatValue = (field?: string | null): string =>
|
||||
field ? chalk.bold(field) : chalk.dim('Unknown');
|
||||
|
||||
const buildDate = new Date(updatedAt);
|
||||
const maybeRuntimeVersion = runtimeVersion ? `Runtime: ${formatValue(runtimeVersion)}` : null;
|
||||
const maybeSdkVersion = sdkVersion ? `SDK: ${formatValue(sdkVersion)}` : null;
|
||||
const appBuildVersionString = `${
|
||||
platform === AppPlatform.Android ? 'Version code' : 'Build number'
|
||||
}: ${formatValue(appBuildVersion)}`;
|
||||
|
||||
const title = [
|
||||
`ID: ${chalk.dim(id)}, Finished at: ${chalk.bold(buildDate.toLocaleString())}`,
|
||||
[
|
||||
`\tApp version: ${formatValue(appVersion)}, ${appBuildVersionString}`,
|
||||
maybeRuntimeVersion,
|
||||
maybeSdkVersion,
|
||||
]
|
||||
.filter(it => it != null)
|
||||
.join(', '),
|
||||
`\tProfile: ${formatValue(buildProfile)}, Release channel: ${formatValue(releaseChannel)}`,
|
||||
`\tAuthored by: ${formatValue(initiatingActor?.displayName)}`,
|
||||
const formattedCommitData =
|
||||
gitCommitHash && gitCommitMessage
|
||||
? `${chalk.dim(gitCommitHash.slice(0, 7))} "${chalk.bold(gitCommitMessage)}"`
|
||||
: 'Unknown';
|
||||
|
||||
const title = `ID: ${chalk.dim(id)} (${chalk.dim(`${fromNow(buildDate)} ago`)})`;
|
||||
|
||||
const description = [
|
||||
`\tProfile: ${formatValue(buildProfile)}`,
|
||||
`\tChannel: ${formatValue(channel)}`,
|
||||
`\tRuntime version: ${formatValue(runtimeVersion)}`,
|
||||
`\tCommit: ${formattedCommitData}`,
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
value: build,
|
||||
disabled: buildDate < expiryDate,
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePromptSourceAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
source: ArchivePromptSource
|
||||
): Promise<Archive> {
|
||||
ctx: ArchiveResolverContext
|
||||
): Promise<ResolvedArchiveSource> {
|
||||
const { sourceType: sourceTypeRaw } = await promptAsync({
|
||||
name: 'sourceType',
|
||||
type: 'select',
|
||||
@@ -372,31 +373,27 @@ async function handlePromptSourceAsync(
|
||||
const sourceType = sourceTypeRaw as ArchiveSourceType;
|
||||
switch (sourceType) {
|
||||
case ArchiveSourceType.url: {
|
||||
const url = await askForArchiveUrlAsync(source.platform);
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
const url = await askForArchiveUrlAsync(ctx.platform);
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url,
|
||||
});
|
||||
}
|
||||
case ArchiveSourceType.path: {
|
||||
const path = await askForArchivePathAsync(source.platform);
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
const path = await askForArchivePathAsync(ctx.platform);
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
});
|
||||
}
|
||||
case ArchiveSourceType.buildList: {
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.buildList,
|
||||
});
|
||||
}
|
||||
case ArchiveSourceType.buildId: {
|
||||
const id = await askForBuildIdAsync();
|
||||
return getArchiveAsync(graphqlClient, {
|
||||
...source,
|
||||
return getArchiveAsync(ctx, {
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id,
|
||||
});
|
||||
@@ -466,13 +463,14 @@ async function askForBuildIdAsync(): Promise<string> {
|
||||
}
|
||||
|
||||
async function askIfUseBuildIdFromUrlAsync(
|
||||
ctx: ArchiveResolverContext,
|
||||
source: ArchiveUrlSource,
|
||||
buildId: string
|
||||
): Promise<boolean> {
|
||||
const { url } = source;
|
||||
Log.warn(`It seems that you provided a build details page URL: ${url}`);
|
||||
Log.warn('We expected to see the build artifact URL.');
|
||||
if (!source.nonInteractive) {
|
||||
if (!ctx.nonInteractive) {
|
||||
const useAsBuildId = await confirmAsync({
|
||||
message: `Do you want to submit build ${buildId} instead?`,
|
||||
});
|
||||
|
||||
@@ -5,12 +5,15 @@ import { withAnalyticsAsync } from '../analytics/common';
|
||||
import {
|
||||
AndroidSubmissionConfigInput,
|
||||
IosSubmissionConfigInput,
|
||||
SubmissionArchiveSourceInput,
|
||||
SubmissionArchiveSourceType,
|
||||
SubmissionFragment,
|
||||
} from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { ora } from '../ora';
|
||||
import { appPlatformDisplayNames } from '../platform';
|
||||
import { ArchiveSourceType, ResolvedArchiveSource } from './ArchiveSource';
|
||||
import { SubmissionContext } from './context';
|
||||
|
||||
export interface SubmissionInput<P extends Platform> {
|
||||
@@ -19,6 +22,7 @@ export interface SubmissionInput<P extends Platform> {
|
||||
? AndroidSubmissionConfigInput
|
||||
: IosSubmissionConfigInput;
|
||||
buildId?: string;
|
||||
archiveSource?: SubmissionArchiveSourceInput;
|
||||
}
|
||||
|
||||
interface AnalyticEvents {
|
||||
@@ -71,6 +75,27 @@ export default abstract class BaseSubmitter<
|
||||
resolvedOptions: ResolvedSourceOptions
|
||||
): Promise<SubmissionInput<P>>;
|
||||
|
||||
public formatArchive(
|
||||
archive: ResolvedArchiveSource
|
||||
): Pick<SubmissionInput<P>, 'archiveSource' | 'buildId'> {
|
||||
switch (archive.sourceType) {
|
||||
case ArchiveSourceType.url: {
|
||||
return { archiveSource: { type: SubmissionArchiveSourceType.Url, url: archive.url } };
|
||||
}
|
||||
case ArchiveSourceType.gcs: {
|
||||
return {
|
||||
archiveSource: {
|
||||
type: SubmissionArchiveSourceType.GcsSubmitArchive,
|
||||
bucketKey: archive.bucketKey,
|
||||
},
|
||||
};
|
||||
}
|
||||
case ArchiveSourceType.build: {
|
||||
return { buildId: archive.build.id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createSubmissionAsync(
|
||||
submissionInput: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment> {
|
||||
|
||||
@@ -4,17 +4,17 @@ import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AppPlatform, BuildFragment, UploadSessionType } from '../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
SubmissionArchiveSourceType,
|
||||
UploadSessionType,
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { uploadFileAtPathToS3Async } from '../../uploads';
|
||||
import {
|
||||
Archive,
|
||||
ArchiveSourceType,
|
||||
BUILD_LIST_ITEM_COUNT,
|
||||
getArchiveAsync,
|
||||
} from '../ArchiveSource';
|
||||
import { uploadFileAtPathToGCSAsync } from '../../uploads';
|
||||
import { ArchiveSourceType, BUILD_LIST_ITEM_COUNT, getArchiveAsync } from '../ArchiveSource';
|
||||
import { getRecentBuildsForSubmissionAsync } from '../utils/builds';
|
||||
|
||||
jest.mock('fs');
|
||||
@@ -28,12 +28,15 @@ jest.mock('../../graphql/queries/BuildQuery', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const ARCHIVE_URL = 'https://url.to/archive.tar.gz';
|
||||
const ARCHIVE_SOURCE = {
|
||||
type: SubmissionArchiveSourceType.Url,
|
||||
url: 'https://url.to/archive.tar.gz',
|
||||
};
|
||||
|
||||
const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
|
||||
id: uuidv4(),
|
||||
artifacts: {
|
||||
buildUrl: ARCHIVE_URL,
|
||||
buildUrl: ARCHIVE_SOURCE.url,
|
||||
},
|
||||
appVersion: '1.2.3',
|
||||
platform: AppPlatform.Android,
|
||||
@@ -67,72 +70,82 @@ describe(getArchiveAsync, () => {
|
||||
});
|
||||
|
||||
it('handles URL source', async () => {
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: ARCHIVE_URL,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: ARCHIVE_SOURCE.url,
|
||||
}
|
||||
);
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('prompts again if provided URL is invalid', async () => {
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: 'invalid',
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: 'invalid',
|
||||
}
|
||||
);
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledTimes(2);
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('asks the user if use build id instead of build details page url', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
||||
jest.mocked(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT as BuildFragment);
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: 'https://expo.dev/accounts/turtle/projects/blah/builds/81da6b36-efe4-4262-8970-84f03efeec81',
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url: 'https://expo.dev/accounts/turtle/projects/blah/builds/81da6b36-efe4-4262-8970-84f03efeec81',
|
||||
}
|
||||
);
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
assertArchiveResult(archive, ArchiveSourceType.buildId);
|
||||
expect((archive.source as any).id).toBe('81da6b36-efe4-4262-8970-84f03efeec81');
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.build);
|
||||
expect((archive as any).build.id).toBe(MOCK_BUILD_FRAGMENT.id);
|
||||
});
|
||||
|
||||
it('handles prompt source', async () => {
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
}
|
||||
);
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles Build ID source', async () => {
|
||||
const buildId = uuidv4();
|
||||
jest.mocked(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT as BuildFragment);
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id: buildId,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id: buildId,
|
||||
}
|
||||
);
|
||||
|
||||
expect(BuildQuery.byIdAsync).toBeCalledWith(graphqlClient, buildId);
|
||||
assertArchiveResult(archive, ArchiveSourceType.buildId);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.build);
|
||||
});
|
||||
|
||||
it('prompts again if build with provided ID doesnt exist', async () => {
|
||||
@@ -140,15 +153,17 @@ describe(getArchiveAsync, () => {
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id: uuidv4(),
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id: uuidv4(),
|
||||
}
|
||||
);
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles latest build source', async () => {
|
||||
@@ -157,18 +172,19 @@ describe(getArchiveAsync, () => {
|
||||
.mocked(getRecentBuildsForSubmissionAsync)
|
||||
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
projectId,
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
|
||||
{
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
}
|
||||
);
|
||||
|
||||
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
|
||||
graphqlClient,
|
||||
toAppPlatform(SOURCE_STUB_INPUT.platform),
|
||||
projectId
|
||||
);
|
||||
assertArchiveResult(archive, ArchiveSourceType.latest);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.build);
|
||||
});
|
||||
|
||||
it('prompts again if no builds exists when selected latest', async () => {
|
||||
@@ -176,14 +192,16 @@ describe(getArchiveAsync, () => {
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
}
|
||||
);
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles build-list-select source', async () => {
|
||||
@@ -193,11 +211,12 @@ describe(getArchiveAsync, () => {
|
||||
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
|
||||
jest.mocked(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
projectId,
|
||||
sourceType: ArchiveSourceType.buildList,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
|
||||
{
|
||||
sourceType: ArchiveSourceType.buildList,
|
||||
}
|
||||
);
|
||||
|
||||
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
|
||||
graphqlClient,
|
||||
@@ -205,7 +224,7 @@ describe(getArchiveAsync, () => {
|
||||
projectId,
|
||||
{ limit: BUILD_LIST_ITEM_COUNT }
|
||||
);
|
||||
assertArchiveResult(archive, ArchiveSourceType.buildList);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.build);
|
||||
});
|
||||
|
||||
it('prompts again if all builds have expired', async () => {
|
||||
@@ -218,14 +237,16 @@ describe(getArchiveAsync, () => {
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.buildList,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.buildList,
|
||||
}
|
||||
);
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('falls back to prompt if user selected "None of the above"', async () => {
|
||||
@@ -236,62 +257,62 @@ describe(getArchiveAsync, () => {
|
||||
.mocked(promptAsync)
|
||||
.mockResolvedValueOnce({ selectedBuild: null })
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.buildList,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.buildList,
|
||||
}
|
||||
);
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
|
||||
it('handles path source', async () => {
|
||||
const path = '/archive.apk';
|
||||
await fs.writeFile(path, 'some content');
|
||||
|
||||
jest
|
||||
.mocked(uploadFileAtPathToS3Async)
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL, bucketKey: 'wat' });
|
||||
jest.mocked(uploadFileAtPathToGCSAsync).mockResolvedValueOnce('wat');
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
}
|
||||
);
|
||||
|
||||
expect(uploadFileAtPathToS3Async).toBeCalledWith(
|
||||
expect(uploadFileAtPathToGCSAsync).toBeCalledWith(
|
||||
graphqlClient,
|
||||
UploadSessionType.EasSubmitAppArchive,
|
||||
UploadSessionType.EasSubmitGcsAppArchive,
|
||||
path,
|
||||
expect.anything()
|
||||
);
|
||||
assertArchiveResult(archive, ArchiveSourceType.path);
|
||||
expect(archive).toMatchObject({
|
||||
sourceType: ArchiveSourceType.gcs,
|
||||
bucketKey: 'wat',
|
||||
localSource: {
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('prompts again if provided path doesnt exist', async () => {
|
||||
jest
|
||||
.mocked(promptAsync)
|
||||
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_URL });
|
||||
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
|
||||
|
||||
const archive = await getArchiveAsync(graphqlClient, {
|
||||
...SOURCE_STUB_INPUT,
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path: './doesnt/exist.aab',
|
||||
});
|
||||
const archive = await getArchiveAsync(
|
||||
{ ...SOURCE_STUB_INPUT, graphqlClient },
|
||||
{
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path: './doesnt/exist.aab',
|
||||
}
|
||||
);
|
||||
|
||||
assertArchiveResult(archive, ArchiveSourceType.url);
|
||||
expect(archive.sourceType).toBe(ArchiveSourceType.url);
|
||||
});
|
||||
});
|
||||
|
||||
function assertArchiveResult(
|
||||
archive: Archive,
|
||||
expectedSourceType: ArchiveSourceType,
|
||||
expectedUrl: string = ARCHIVE_URL
|
||||
): void {
|
||||
expect(archive.source.sourceType).toBe(expectedSourceType);
|
||||
if (archive.url) {
|
||||
expect(archive.url).toBe(expectedUrl);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export default class AndroidSubmitCommand {
|
||||
|
||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||
try {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.ANDROID));
|
||||
return result(resolveArchiveSource(this.ctx));
|
||||
} catch (err: any) {
|
||||
return result(err);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import { ArchiveSource, ResolvedArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
|
||||
import { SubmissionContext } from '../context';
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ export interface AndroidSubmissionOptions
|
||||
}
|
||||
|
||||
interface ResolvedSourceOptions {
|
||||
archive: Archive;
|
||||
archive: ResolvedArchiveSource;
|
||||
serviceAccountKeyResult: ServiceAccountKeyResult;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,16 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
constructor(ctx: SubmissionContext<Platform.ANDROID>, options: AndroidSubmissionOptions) {
|
||||
const sourceOptionsResolver = {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
archive: async () => await getArchiveAsync(ctx.graphqlClient, this.options.archiveSource),
|
||||
archive: async () =>
|
||||
await getArchiveAsync(
|
||||
{
|
||||
graphqlClient: ctx.graphqlClient,
|
||||
platform: Platform.ANDROID,
|
||||
projectId: ctx.projectId,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
},
|
||||
this.options.archiveSource
|
||||
),
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
serviceAccountKeyResult: async () => {
|
||||
return await getServiceAccountKeyResultAsync(this.ctx, this.options.serviceAccountSource);
|
||||
@@ -71,7 +80,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
public async createSubmissionInputAsync(
|
||||
resolvedSourceOptions: ResolvedSourceOptions
|
||||
): Promise<SubmissionInput<Platform.ANDROID>> {
|
||||
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
|
||||
const submissionConfig = this.formatSubmissionConfig(this.options, resolvedSourceOptions);
|
||||
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
@@ -80,7 +89,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
return {
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
buildId: resolvedSourceOptions.archive.build?.id,
|
||||
...this.formatArchive(resolvedSourceOptions.archive),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,21 +97,22 @@ export default class AndroidSubmitter extends BaseSubmitter<
|
||||
projectId,
|
||||
submissionConfig,
|
||||
buildId,
|
||||
archiveSource,
|
||||
}: SubmissionInput<Platform.ANDROID>): Promise<SubmissionFragment> {
|
||||
return await SubmissionMutation.createAndroidSubmissionAsync(this.ctx.graphqlClient, {
|
||||
appId: projectId,
|
||||
config: submissionConfig,
|
||||
submittedBuildId: buildId,
|
||||
archiveSource,
|
||||
});
|
||||
}
|
||||
|
||||
private formatSubmissionConfig(
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, serviceAccountKeyResult }: ResolvedSourceOptions
|
||||
{ serviceAccountKeyResult }: ResolvedSourceOptions
|
||||
): AndroidSubmissionConfigInput {
|
||||
const { track, releaseStatus, changesNotSentForReview } = options;
|
||||
return {
|
||||
archiveUrl: archive.url,
|
||||
track,
|
||||
changesNotSentForReview,
|
||||
releaseStatus,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
BuildFragment,
|
||||
SubmissionAndroidReleaseStatus,
|
||||
SubmissionAndroidTrack,
|
||||
SubmissionArchiveSourceType,
|
||||
} from '../../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
@@ -104,7 +105,7 @@ describe(AndroidSubmitCommand, () => {
|
||||
});
|
||||
|
||||
describe('sending submission', () => {
|
||||
it('sends a request to Submission Service', async () => {
|
||||
it('sends a request to EAS Submit', async () => {
|
||||
const projectId = uuidv4();
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const analytics = instance(mock<Analytics>());
|
||||
@@ -134,8 +135,8 @@ describe(AndroidSubmitCommand, () => {
|
||||
|
||||
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
archiveSource: { type: SubmissionArchiveSourceType.Url, url: 'http://expo.dev/fake.apk' },
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.apk',
|
||||
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
|
||||
track: SubmissionAndroidTrack.Internal,
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Platform } from '@expo/eas-build-job';
|
||||
import { ArchiveSource, ArchiveSourceType, isUuidV4 } from './ArchiveSource';
|
||||
import { SubmissionContext } from './context';
|
||||
|
||||
export function resolveArchiveSource<T extends Platform>(
|
||||
ctx: SubmissionContext<T>,
|
||||
platform: T
|
||||
): ArchiveSource {
|
||||
export function resolveArchiveSource<T extends Platform>(ctx: SubmissionContext<T>): ArchiveSource {
|
||||
const { url, path, id, latest } = ctx.archiveFlags;
|
||||
const chosenOptions = [url, path, id, latest];
|
||||
if (chosenOptions.filter(opt => opt).length > 1) {
|
||||
@@ -17,17 +14,11 @@ export function resolveArchiveSource<T extends Platform>(
|
||||
return {
|
||||
sourceType: ArchiveSourceType.url,
|
||||
url,
|
||||
platform,
|
||||
projectId: ctx.projectId,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
};
|
||||
} else if (path) {
|
||||
return {
|
||||
sourceType: ArchiveSourceType.path,
|
||||
path,
|
||||
platform,
|
||||
projectId: ctx.projectId,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
};
|
||||
} else if (id) {
|
||||
if (!isUuidV4(id)) {
|
||||
@@ -36,25 +27,16 @@ export function resolveArchiveSource<T extends Platform>(
|
||||
return {
|
||||
sourceType: ArchiveSourceType.buildId,
|
||||
id,
|
||||
platform,
|
||||
projectId: ctx.projectId,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
};
|
||||
} else if (latest) {
|
||||
return {
|
||||
sourceType: ArchiveSourceType.latest,
|
||||
platform,
|
||||
projectId: ctx.projectId,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
};
|
||||
} else if (ctx.nonInteractive) {
|
||||
throw new Error('You need to specify the archive source when running in non-interactive mode ');
|
||||
} else {
|
||||
return {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
platform,
|
||||
projectId: ctx.projectId,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ export default class IosSubmitCommand {
|
||||
|
||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||
try {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.IOS));
|
||||
return result(resolveArchiveSource(this.ctx));
|
||||
} catch (err: any) {
|
||||
return result(err);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MinimalAscApiKey } from '../../credentials/ios/credentials';
|
||||
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import { ArchiveSource, ResolvedArchiveSource, getArchiveAsync } from '../ArchiveSource';
|
||||
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
|
||||
import { SubmissionContext } from '../context';
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ export interface IosSubmissionOptions
|
||||
}
|
||||
|
||||
interface ResolvedSourceOptions {
|
||||
archive: Archive;
|
||||
archive: ResolvedArchiveSource;
|
||||
credentials: {
|
||||
appSpecificPassword?: AppSpecificPasswordCredentials;
|
||||
ascApiKeyResult?: AscApiKeyResult;
|
||||
@@ -55,7 +55,16 @@ export default class IosSubmitter extends BaseSubmitter<
|
||||
constructor(ctx: SubmissionContext<Platform.IOS>, options: IosSubmissionOptions) {
|
||||
const sourceOptionsResolver = {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
archive: async () => await getArchiveAsync(ctx.graphqlClient, this.options.archiveSource),
|
||||
archive: async () =>
|
||||
await getArchiveAsync(
|
||||
{
|
||||
graphqlClient: ctx.graphqlClient,
|
||||
platform: Platform.IOS,
|
||||
projectId: ctx.projectId,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
},
|
||||
this.options.archiveSource
|
||||
),
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
credentials: async () => {
|
||||
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
|
||||
@@ -105,7 +114,7 @@ export default class IosSubmitter extends BaseSubmitter<
|
||||
return {
|
||||
projectId: this.options.projectId,
|
||||
submissionConfig,
|
||||
buildId: resolvedSourceOptions.archive.build?.id,
|
||||
...this.formatArchive(resolvedSourceOptions.archive),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,24 +122,25 @@ export default class IosSubmitter extends BaseSubmitter<
|
||||
projectId,
|
||||
submissionConfig,
|
||||
buildId,
|
||||
archiveSource,
|
||||
}: SubmissionInput<Platform.IOS>): Promise<SubmissionFragment> {
|
||||
return await SubmissionMutation.createIosSubmissionAsync(this.ctx.graphqlClient, {
|
||||
appId: projectId,
|
||||
config: submissionConfig,
|
||||
submittedBuildId: buildId,
|
||||
archiveSource,
|
||||
});
|
||||
}
|
||||
|
||||
private formatSubmissionConfig(
|
||||
options: IosSubmissionOptions,
|
||||
{ archive, credentials }: ResolvedSourceOptions
|
||||
{ credentials }: ResolvedSourceOptions
|
||||
): IosSubmissionConfigInput {
|
||||
const { appSpecificPassword, ascApiKeyResult } = credentials;
|
||||
const { appleIdUsername, ascAppIdentifier } = options;
|
||||
return {
|
||||
ascAppIdentifier,
|
||||
appleIdUsername,
|
||||
archiveUrl: archive.url,
|
||||
...(appSpecificPassword ? this.formatAppSpecificPassword(appSpecificPassword) : null),
|
||||
...(ascApiKeyResult?.result ? this.formatAscApiKeyResult(ascApiKeyResult.result) : null),
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
jester as mockJester,
|
||||
testProjectId,
|
||||
} from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { SubmissionArchiveSourceType } from '../../../graphql/generated';
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
|
||||
@@ -80,7 +81,7 @@ describe(IosSubmitCommand, () => {
|
||||
});
|
||||
|
||||
describe('sending submission', () => {
|
||||
it('sends a request to Submission Service', async () => {
|
||||
it('sends a request to EAS Submit', async () => {
|
||||
const projectId = uuidv4();
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const analytics = instance(mock<Analytics>());
|
||||
@@ -110,8 +111,8 @@ describe(IosSubmitCommand, () => {
|
||||
|
||||
expect(SubmissionMutation.createIosSubmissionAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
archiveSource: { type: SubmissionArchiveSourceType.Url, url: 'http://expo.dev/fake.ipa' },
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.ipa',
|
||||
appleIdUsername: '[email protected]',
|
||||
appleAppSpecificPassword: 'supersecret',
|
||||
ascAppIdentifier: '12345678',
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { UploadSessionType } from '../../graphql/generated';
|
||||
import { uploadFileAtPathToS3Async } from '../../uploads';
|
||||
import { uploadFileAtPathToGCSAsync } from '../../uploads';
|
||||
import { createProgressTracker } from '../../utils/progress';
|
||||
|
||||
export async function isExistingFileAsync(filePath: string): Promise<boolean> {
|
||||
@@ -19,9 +19,9 @@ export async function uploadAppArchiveAsync(
|
||||
path: string
|
||||
): Promise<string> {
|
||||
const fileSize = (await fs.stat(path)).size;
|
||||
const { url } = await uploadFileAtPathToS3Async(
|
||||
const bucketKey = await uploadFileAtPathToGCSAsync(
|
||||
graphqlClient,
|
||||
UploadSessionType.EasSubmitAppArchive,
|
||||
UploadSessionType.EasSubmitGcsAppArchive,
|
||||
path,
|
||||
createProgressTracker({
|
||||
total: fileSize,
|
||||
@@ -29,5 +29,5 @@ export async function uploadAppArchiveAsync(
|
||||
completedMessage: 'Uploaded to EAS Submit',
|
||||
})
|
||||
);
|
||||
return url;
|
||||
return bucketKey;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { Archive, ArchiveSourceType } from '../ArchiveSource';
|
||||
import { ArchiveSourceType, ResolvedArchiveSource } from '../ArchiveSource';
|
||||
export interface ArchiveSourceSummaryFields {
|
||||
archiveUrl?: string;
|
||||
archivePath?: string;
|
||||
@@ -43,19 +43,20 @@ function formatSubmissionBuildSummary(build: BuildFragment): string {
|
||||
);
|
||||
}
|
||||
|
||||
export function formatArchiveSourceSummary({ source, build }: Archive): ArchiveSourceSummaryFields {
|
||||
export function formatArchiveSourceSummary(
|
||||
archive: ResolvedArchiveSource
|
||||
): ArchiveSourceSummaryFields {
|
||||
const summarySlice: ArchiveSourceSummaryFields = {};
|
||||
|
||||
switch (source.sourceType) {
|
||||
case ArchiveSourceType.path:
|
||||
summarySlice.archivePath = source.path;
|
||||
switch (archive.sourceType) {
|
||||
case ArchiveSourceType.gcs:
|
||||
summarySlice.archivePath = archive.localSource.path;
|
||||
break;
|
||||
case ArchiveSourceType.url:
|
||||
summarySlice.archiveUrl = source.url;
|
||||
summarySlice.archiveUrl = archive.url;
|
||||
break;
|
||||
case ArchiveSourceType.buildId:
|
||||
case ArchiveSourceType.latest:
|
||||
summarySlice.formattedBuild = formatSubmissionBuildSummary(build!);
|
||||
case ArchiveSourceType.build:
|
||||
summarySlice.formattedBuild = formatSubmissionBuildSummary(archive.build!);
|
||||
break;
|
||||
}
|
||||
return summarySlice;
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
@@ -1,29 +1,18 @@
|
||||
import { truncatePublishUpdateMessage } from '../../commands/update';
|
||||
import Log from '../../log';
|
||||
import { getPlatformsForGroup } from '../utils';
|
||||
import { getPlatformsForGroup, truncateString } from '../utils';
|
||||
|
||||
describe('update utility functions', () => {
|
||||
describe(truncatePublishUpdateMessage.name, () => {
|
||||
const warnSpy = jest.spyOn(Log, 'warn');
|
||||
beforeEach(() => {
|
||||
warnSpy.mockClear();
|
||||
});
|
||||
|
||||
describe(truncateString, () => {
|
||||
it('does not alter messages with less than 1024 characters', () => {
|
||||
const message = 'Small message =)';
|
||||
const truncatedMessage = truncatePublishUpdateMessage(message);
|
||||
const truncatedMessage = truncateString(message, 1024);
|
||||
expect(truncatedMessage).toEqual(message);
|
||||
expect(warnSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('truncates messages to a length of 1024, including ellipses', () => {
|
||||
const longMessage = Array.from({ length: 2024 }, () => 'a').join('');
|
||||
const truncatedMessage = truncatePublishUpdateMessage(longMessage);
|
||||
const truncatedMessage = truncateString(longMessage, 1024);
|
||||
expect(truncatedMessage.length).toEqual(1024);
|
||||
expect(truncatedMessage.slice(-3)).toEqual('...');
|
||||
expect(warnSpy).toBeCalledWith(
|
||||
'Update message exceeds the allowed 1024 character limit. Truncating message...'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
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';
|
||||
import { AppPlatform } from '../graphql/generated';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { RequestedPlatform, appPlatformDisplayNames } from '../platform';
|
||||
import {
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
} from '../project/projectUtils';
|
||||
import { resolveWorkflowPerPlatformAsync } from '../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from './android/UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from './ios/UpdatesModule';
|
||||
|
||||
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
|
||||
export const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
|
||||
|
||||
function getDefaultRuntimeVersion(workflow: Workflow): NonNullable<ExpoConfig['runtimeVersion']> {
|
||||
return workflow === Workflow.GENERIC
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
}
|
||||
|
||||
function isRuntimeEqual(
|
||||
runtimeVersionA: NonNullable<ExpoConfig['runtimeVersion']>,
|
||||
runtimeVersionB: NonNullable<ExpoConfig['runtimeVersion']>
|
||||
): boolean {
|
||||
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
|
||||
return runtimeVersionA === runtimeVersionB;
|
||||
} else if (typeof runtimeVersionA === 'object' && typeof runtimeVersionB === 'object') {
|
||||
return runtimeVersionA.policy === runtimeVersionB.policy;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceUndefinedObjectValues(
|
||||
value: Record<string, any>,
|
||||
replacement: any
|
||||
): Record<string, any> {
|
||||
for (const key in value) {
|
||||
if (value[key] === undefined) {
|
||||
value[key] = replacement;
|
||||
} else if (typeof value[key] === 'object') {
|
||||
value[key] = replaceUndefinedObjectValues(value[key], replacement);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Partially merge the EAS Update config with the existing Expo config.
|
||||
* This preserves and merges the nested update-related properties.
|
||||
*/
|
||||
function mergeExpoConfig(exp: ExpoConfig, modifyExp: Partial<ExpoConfig>): Partial<ExpoConfig> {
|
||||
return {
|
||||
runtimeVersion: modifyExp.runtimeVersion ?? exp.runtimeVersion,
|
||||
updates: { ...exp.updates, ...modifyExp.updates },
|
||||
android: {
|
||||
...exp.android,
|
||||
...modifyExp.android,
|
||||
},
|
||||
ios: {
|
||||
...exp.ios,
|
||||
...modifyExp.ios,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the `app.json` is configured to use EAS Updates.
|
||||
* This does a couple of things:
|
||||
* - Ensure update URL is set to the project EAS endpoint
|
||||
* - Ensure runtimeVersion is defined for both or individual platforms
|
||||
* - Output the changes made, or the changes required to make manually
|
||||
*/
|
||||
async function ensureEASUpdatesIsConfiguredInExpoConfigAsync({
|
||||
exp,
|
||||
projectId,
|
||||
projectDir,
|
||||
platform,
|
||||
workflows,
|
||||
}: {
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
platform: RequestedPlatform;
|
||||
workflows: Record<Platform, Workflow>;
|
||||
}): Promise<{ projectChanged: boolean; exp: ExpoConfig }> {
|
||||
const modifyConfig: Partial<ExpoConfig> = {};
|
||||
|
||||
if (exp.updates?.url !== getEASUpdateURL(projectId)) {
|
||||
modifyConfig.updates = { url: getEASUpdateURL(projectId) };
|
||||
}
|
||||
|
||||
let androidRuntimeVersion = exp.android?.runtimeVersion ?? exp.runtimeVersion;
|
||||
let iosRuntimeVersion = exp.ios?.runtimeVersion ?? exp.runtimeVersion;
|
||||
|
||||
if (
|
||||
(['all', 'android'].includes(platform) && !androidRuntimeVersion) ||
|
||||
(['all', 'ios'].includes(platform) && !iosRuntimeVersion)
|
||||
) {
|
||||
androidRuntimeVersion = androidRuntimeVersion ?? getDefaultRuntimeVersion(workflows.android);
|
||||
iosRuntimeVersion = iosRuntimeVersion ?? getDefaultRuntimeVersion(workflows.ios);
|
||||
|
||||
if (platform === 'all' && isRuntimeEqual(androidRuntimeVersion, iosRuntimeVersion)) {
|
||||
modifyConfig.runtimeVersion = androidRuntimeVersion;
|
||||
} else {
|
||||
if (['all', 'android'].includes(platform)) {
|
||||
modifyConfig.runtimeVersion = undefined;
|
||||
modifyConfig.android = { runtimeVersion: androidRuntimeVersion };
|
||||
}
|
||||
if (['all', 'ios'].includes(platform)) {
|
||||
modifyConfig.runtimeVersion = undefined;
|
||||
modifyConfig.ios = { runtimeVersion: iosRuntimeVersion };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(modifyConfig).length === 0) {
|
||||
return { exp, projectChanged: false };
|
||||
}
|
||||
|
||||
const mergedExp = mergeExpoConfig(exp, modifyConfig);
|
||||
const result = await modifyConfigAsync(projectDir, mergedExp);
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
logEasUpdatesAutoConfig({ exp, modifyConfig });
|
||||
return {
|
||||
projectChanged: true,
|
||||
// TODO(cedric): fix return type of `modifyConfigAsync` to avoid `null` for type === success repsonses
|
||||
exp: result.config?.expo!,
|
||||
};
|
||||
|
||||
case 'warn':
|
||||
warnEASUpdatesManualConfig({ modifyConfig, workflows });
|
||||
throw new Error(result.message);
|
||||
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unexpected result type "${result.type}" received when modifying the project config.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function logEasUpdatesAutoConfig({
|
||||
modifyConfig,
|
||||
exp,
|
||||
}: {
|
||||
modifyConfig: Partial<ExpoConfig>;
|
||||
exp: ExpoConfig;
|
||||
}): void {
|
||||
if (modifyConfig.updates?.url) {
|
||||
Log.withTick(
|
||||
exp.updates?.url
|
||||
? `Overwrote updates.url "${exp.updates.url}" with "${modifyConfig.updates.url}"`
|
||||
: `Configured updates.url to "${modifyConfig.updates.url}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (modifyConfig.android?.runtimeVersion ?? modifyConfig.runtimeVersion) {
|
||||
Log.withTick(
|
||||
`Configured runtimeVersion for ${
|
||||
appPlatformDisplayNames[AppPlatform.Android]
|
||||
} with "${JSON.stringify(
|
||||
modifyConfig.android?.runtimeVersion ?? modifyConfig.runtimeVersion
|
||||
)}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (modifyConfig.ios?.runtimeVersion ?? modifyConfig.runtimeVersion) {
|
||||
Log.withTick(
|
||||
`Configured runtimeVersion for ${
|
||||
appPlatformDisplayNames[AppPlatform.Ios]
|
||||
} with "${JSON.stringify(modifyConfig.ios?.runtimeVersion ?? modifyConfig.runtimeVersion)}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function warnEASUpdatesManualConfig({
|
||||
modifyConfig,
|
||||
workflows,
|
||||
}: {
|
||||
modifyConfig: Partial<ExpoConfig>;
|
||||
workflows: Record<Platform, Workflow>;
|
||||
}): void {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`It looks like you are using a dynamic configuration! ${learnMore(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`Finish configuring EAS Update by adding the following to the project app.config.js:\n${learnMore(
|
||||
'https://expo.fyi/eas-update-config'
|
||||
)}\n`
|
||||
);
|
||||
|
||||
Log.log(
|
||||
chalk.bold(
|
||||
JSON.stringify(replaceUndefinedObjectValues(modifyConfig, '<remove this key>'), null, 2)
|
||||
)
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
if (workflows.android === Workflow.GENERIC || workflows.ios === Workflow.GENERIC) {
|
||||
Log.warn(
|
||||
chalk`The native config files {bold Expo.plist & AndroidManifest.xml} must be updated to support EAS Update. ${learnMore(
|
||||
'https://expo.fyi/eas-update-config.md#native-configuration'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that the current `app.json` configuration for EAS Updates is set natively.
|
||||
*/
|
||||
async function ensureEASUpdateIsConfiguredNativelyAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
exp,
|
||||
projectId,
|
||||
projectDir,
|
||||
platform,
|
||||
workflows,
|
||||
}: {
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
platform: RequestedPlatform;
|
||||
workflows: Record<Platform, Workflow>;
|
||||
}
|
||||
): Promise<void> {
|
||||
if (['all', 'android'].includes(platform) && workflows.android === Workflow.GENERIC) {
|
||||
await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
|
||||
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
|
||||
}
|
||||
|
||||
if (['all', 'ios'].includes(platform) && workflows.ios === Workflow.GENERIC) {
|
||||
await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
|
||||
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
* - Enure the `expo-updates` package is currently installed.
|
||||
* - Ensure `app.json` is configured for EAS Updates
|
||||
* - Sets `runtimeVersion` if not set
|
||||
* - Sets `updates.url` if not set
|
||||
* - Ensure latest changes are reflected in the native config, if any
|
||||
*/
|
||||
export async function ensureEASUpdateIsConfiguredAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
exp: expWithoutUpdates,
|
||||
projectId,
|
||||
projectDir,
|
||||
platform,
|
||||
}: {
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
platform: RequestedPlatform | null;
|
||||
}
|
||||
): Promise<void> {
|
||||
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(
|
||||
projectDir,
|
||||
expWithoutUpdates.sdkVersion
|
||||
);
|
||||
if (!hasExpoUpdates) {
|
||||
await installExpoUpdatesAsync(projectDir, { silent: !Log.isDebug });
|
||||
Log.withTick('Installed expo updates');
|
||||
}
|
||||
|
||||
// Bail out if using a platform that doesn't require runtime versions
|
||||
// or native setup, i.e. web.
|
||||
if (!platform) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workflows = await resolveWorkflowPerPlatformAsync(projectDir);
|
||||
const { projectChanged, exp: expWithUpdates } =
|
||||
await ensureEASUpdatesIsConfiguredInExpoConfigAsync({
|
||||
exp: expWithoutUpdates,
|
||||
projectDir,
|
||||
projectId,
|
||||
platform,
|
||||
workflows,
|
||||
});
|
||||
|
||||
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
|
||||
|
||||
if (projectChanged || !hasExpoUpdates) {
|
||||
await ensureEASUpdateIsConfiguredNativelyAsync(graphqlClient, {
|
||||
exp: expWithUpdates,
|
||||
projectDir,
|
||||
projectId,
|
||||
platform,
|
||||
workflows,
|
||||
});
|
||||
}
|
||||
|
||||
if (projectChanged) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`All builds of your app going forward will be eligible to receive updates published with EAS Update.`
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
import assert from 'assert';
|
||||
import FormData from 'form-data';
|
||||
import fs from 'fs-extra';
|
||||
import { Response } from 'node-fetch';
|
||||
import nullthrows from 'nullthrows';
|
||||
import promiseRetry from 'promise-retry';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { ExpoGraphqlClient } from './commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import fetch from './fetch';
|
||||
import { UploadSessionType } from './graphql/generated';
|
||||
import { PresignedPost, UploadSessionMutation } from './graphql/mutations/UploadSessionMutation';
|
||||
import { SignedUrl, UploadSessionMutation } from './graphql/mutations/UploadSessionMutation';
|
||||
import { ProgressHandler } from './utils/progress';
|
||||
|
||||
export async function uploadFileAtPathToS3Async(
|
||||
export interface PresignedPost {
|
||||
url: string;
|
||||
fields: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function uploadFileAtPathToGCSAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
type: UploadSessionType,
|
||||
path: string,
|
||||
handleProgressEvent: ProgressHandler
|
||||
): Promise<{ url: string; bucketKey: string }> {
|
||||
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(graphqlClient, type);
|
||||
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
|
||||
): Promise<string> {
|
||||
const signedUrl = await UploadSessionMutation.createUploadSessionAsync(graphqlClient, type);
|
||||
|
||||
const response = await uploadWithPresignedPostWithProgressAsync(
|
||||
path,
|
||||
presignedPost,
|
||||
handleProgressEvent
|
||||
);
|
||||
const location = nullthrows(
|
||||
response.headers.get('location'),
|
||||
`location does not exist in response headers (make sure you're uploading to AWS S3)`
|
||||
);
|
||||
const url = fixS3Url(location);
|
||||
return { url, bucketKey: presignedPost.fields.key };
|
||||
await uploadWithSignedUrlWithProgressAsync(path, signedUrl, handleProgressEvent);
|
||||
return signedUrl.bucketKey;
|
||||
}
|
||||
|
||||
export async function uploadWithPresignedPostWithRetryAsync(
|
||||
@@ -72,10 +64,10 @@ export async function uploadWithPresignedPostWithRetryAsync(
|
||||
);
|
||||
}
|
||||
|
||||
async function createPresignedPostFormDataAsync(
|
||||
async function uploadWithPresignedPostAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost
|
||||
): Promise<{ form: FormData; fileSize: number }> {
|
||||
): Promise<Response> {
|
||||
const fileStat = await fs.stat(file);
|
||||
const fileSize = fileStat.size;
|
||||
const form = new FormData();
|
||||
@@ -83,14 +75,6 @@ async function createPresignedPostFormDataAsync(
|
||||
form.append(fieldKey, fieldValue);
|
||||
}
|
||||
form.append('file', fs.createReadStream(file), { knownLength: fileSize });
|
||||
return { form, fileSize };
|
||||
}
|
||||
|
||||
async function uploadWithPresignedPostAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost
|
||||
): Promise<Response> {
|
||||
const { form } = await createPresignedPostFormDataAsync(file, presignedPost);
|
||||
const formHeaders = form.getHeaders();
|
||||
return await fetch(presignedPost.url, {
|
||||
method: 'POST',
|
||||
@@ -101,23 +85,25 @@ async function uploadWithPresignedPostAsync(
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadWithPresignedPostWithProgressAsync(
|
||||
async function uploadWithSignedUrlWithProgressAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost,
|
||||
signedUrl: SignedUrl,
|
||||
handleProgressEvent: ProgressHandler
|
||||
): Promise<Response> {
|
||||
const { form, fileSize } = await createPresignedPostFormDataAsync(file, presignedPost);
|
||||
const formHeaders = form.getHeaders();
|
||||
const uploadPromise = fetch(presignedPost.url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
const fileStat = await fs.stat(file);
|
||||
const fileSize = fileStat.size;
|
||||
|
||||
const readStream = fs.createReadStream(file);
|
||||
const uploadPromise = fetch(signedUrl.url, {
|
||||
method: 'PUT',
|
||||
body: readStream,
|
||||
headers: {
|
||||
...formHeaders,
|
||||
...signedUrl.headers,
|
||||
},
|
||||
});
|
||||
|
||||
let currentSize = 0;
|
||||
form.addListener('data', (chunk: Buffer) => {
|
||||
readStream.addListener('data', (chunk: Buffer) => {
|
||||
currentSize += Buffer.byteLength(chunk);
|
||||
handleProgressEvent({
|
||||
progress: {
|
||||
@@ -136,14 +122,3 @@ async function uploadWithPresignedPostWithProgressAsync(
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* S3 returns broken URLs, sth like:
|
||||
* https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c
|
||||
* This function replaces %2F with /.
|
||||
*/
|
||||
export function fixS3Url(archiveUrl: string): string {
|
||||
const parsed = new URL(archiveUrl);
|
||||
parsed.pathname = decodeURIComponent(parsed.pathname);
|
||||
return parsed.toString();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { shouldUseVersionedExpoCLIExpensive } from '../expoCli';
|
||||
|
||||
jest.mock('resolve-from', () => ({
|
||||
silent: jest.fn(() => '/path/to/@expo/cli'),
|
||||
}));
|
||||
|
||||
const originalEnv = process.env;
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
const DEFAULT_CONFIG = { sdkVersion: '46.0.0' };
|
||||
|
||||
describe(shouldUseVersionedExpoCLIExpensive, () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.EXPO_USE_LOCAL_CLI;
|
||||
});
|
||||
|
||||
// There are meany conditions and all should be enabled by default in this
|
||||
// describe block.
|
||||
it(`returns true if all conditions are met (sanity)`, () => {
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', DEFAULT_CONFIG)).toBe(true);
|
||||
});
|
||||
|
||||
it(`returns true if EXPO_USE_LOCAL_CLI is unset`, () => {
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', DEFAULT_CONFIG)).toBe(true);
|
||||
});
|
||||
|
||||
it(`returns false if EXPO_USE_LOCAL_CLI is set to false`, () => {
|
||||
['false', '0'].forEach(falsey => {
|
||||
process.env.EXPO_USE_LOCAL_CLI = falsey;
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', DEFAULT_CONFIG)).toBe(false);
|
||||
});
|
||||
});
|
||||
it(`returns true if EXPO_USE_LOCAL_CLI is set to a truthy value`, () => {
|
||||
['true', '1'].forEach(truthy => {
|
||||
process.env.EXPO_USE_LOCAL_CLI = truthy;
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', DEFAULT_CONFIG)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it(`returns false if versioned CLI isn't installed`, () => {
|
||||
jest.mocked(resolveFrom.silent).mockImplementationOnce(() => undefined);
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', DEFAULT_CONFIG)).toBe(false);
|
||||
});
|
||||
|
||||
it(`returns false if the Expo SDK version is less than 46`, () => {
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', { sdkVersion: '45.0.0' })).toBe(false);
|
||||
});
|
||||
it(`returns true if the Expo SDK version is UNVERSIONED`, () => {
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', { sdkVersion: 'UNVERSIONED' })).toBe(true);
|
||||
});
|
||||
it(`returns true if the Expo SDK version is an invalid format`, () => {
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', { sdkVersion: 'foobar' })).toBe(true);
|
||||
});
|
||||
it(`returns true if the Expo SDK version is undefined`, () => {
|
||||
expect(shouldUseVersionedExpoCLIExpensive('/', { sdkVersion: undefined })).toBe(true);
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,59 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
|
||||
import { ImageNonPngError, ImageTransparencyError, ensurePNGIsNotTransparentAsync } from '../image';
|
||||
|
||||
const TEST_SERVER_PORT = 2137;
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
const transparentPngPath = path.join(fixturesPath, 'icon-alpha.png');
|
||||
const nonTransparentPngPath = path.join(fixturesPath, 'icon-no-alpha.png');
|
||||
const jpgPath = path.join(fixturesPath, 'icon.jpg');
|
||||
const nonTransparentPngURL = `http://127.0.0.1:${TEST_SERVER_PORT}/icon-alpha.png`;
|
||||
|
||||
describe(ensurePNGIsNotTransparentAsync, () => {
|
||||
describe('local paths', () => {
|
||||
it('throws an error for non-PNG files', async () => {
|
||||
await expect(ensurePNGIsNotTransparentAsync(jpgPath)).rejects.toThrowError(ImageNonPngError);
|
||||
});
|
||||
|
||||
it('throws an error for transparent PNGs', async () => {
|
||||
await expect(ensurePNGIsNotTransparentAsync(transparentPngPath)).rejects.toThrowError(
|
||||
ImageTransparencyError
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw for non transparent PNGs', async () => {
|
||||
await expect(ensurePNGIsNotTransparentAsync(nonTransparentPngPath)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote URLs', () => {
|
||||
let server: http.Server;
|
||||
|
||||
beforeAll(async () => {
|
||||
await new Promise<void>(res => {
|
||||
const app = express();
|
||||
app.use(express.static(fixturesPath));
|
||||
server = app.listen(TEST_SERVER_PORT, () => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise<void>(res => {
|
||||
server.close(() => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works with remote URLs', async () => {
|
||||
await expect(ensurePNGIsNotTransparentAsync(nonTransparentPngURL)).rejects.toThrowError(
|
||||
ImageTransparencyError
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -1,8 +1,62 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import { boolish } from 'getenv';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../log';
|
||||
import { memoize } from './expodash/memoize';
|
||||
|
||||
// Aggressively returns `true` (UNVERSIONED, invalid SDK version format) to push users towards the versioned CLI.
|
||||
function gteSdkVersion(fromSdkVersion: string, sdkVersion: string): boolean {
|
||||
if (fromSdkVersion === 'UNVERSIONED') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return semver.gte(fromSdkVersion, sdkVersion);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if the project is SDK +46, has `@expo/cli`, and `EXPO_USE_LOCAL_CLI` is not set to a _false_ value.
|
||||
*/
|
||||
export function shouldUseVersionedExpoCLIExpensive(
|
||||
projectDir: string,
|
||||
exp: Pick<ExpoConfig, 'sdkVersion'>
|
||||
): boolean {
|
||||
// Users can disable local CLI settings EXPO_USE_LOCAL_CLI=false
|
||||
// https://github.com/expo/expo/blob/69eddda7bb1dbfab44258f468cf7f22984c1e44e/packages/expo/bin/cli.js#L10
|
||||
// Run the environment variable check first as it's the cheapest.
|
||||
const userDefinedVersionedCliEnabled = boolish('EXPO_USE_LOCAL_CLI', true);
|
||||
if (!userDefinedVersionedCliEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// NOTE(EvanBacon): The CLI package could be available through a monorepo
|
||||
// we need to ensure the project is specifically using a known supported Expo SDK version.
|
||||
|
||||
if (
|
||||
// If the version isn't defined then skip the check.
|
||||
// Users running in a non-standard way should use the latest supported behavior (local CLI).
|
||||
exp.sdkVersion &&
|
||||
!gteSdkVersion(exp.sdkVersion, '46.0.0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
Log.debug('Error detecting Expo SDK version for enabling versioned Expo CLI:', error);
|
||||
}
|
||||
|
||||
// Finally ensure the CLI is available for sanity.
|
||||
return !!resolveFrom.silent(projectDir, '@expo/cli');
|
||||
}
|
||||
|
||||
export const shouldUseVersionedExpoCLI = memoize(shouldUseVersionedExpoCLIExpensive);
|
||||
|
||||
export async function expoCommandAsync(
|
||||
projectDir: string,
|
||||
|
||||
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user