mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cad9578c22 | ||
|
|
7b21f9d802 | ||
|
|
2f7d857bec | ||
|
|
7994a2b33e | ||
|
|
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 |
No files matched your search
+72
-1
@@ -8,15 +8,86 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 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
|
||||
|
||||
@@ -267,7 +338,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.0.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 .",
|
||||
|
||||
+154
-54
@@ -70,6 +70,7 @@ eas --help COMMAND
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:inspect`](#eas-buildinspect)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:run`](#eas-buildrun)
|
||||
* [`eas build:submit`](#eas-buildsubmit)
|
||||
* [`eas build:version:set`](#eas-buildversionset)
|
||||
* [`eas build:version:sync`](#eas-buildversionsync)
|
||||
@@ -95,6 +96,8 @@ eas --help COMMAND
|
||||
* [`eas open`](#eas-open)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas run`](#eas-run)
|
||||
* [`eas run:run`](#eas-runrun)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
@@ -104,6 +107,7 @@ eas --help COMMAND
|
||||
* [`eas update:configure`](#eas-updateconfigure)
|
||||
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
|
||||
* [`eas update:list`](#eas-updatelist)
|
||||
* [`eas update:republish`](#eas-updaterepublish)
|
||||
* [`eas update:view GROUPID`](#eas-updateview-groupid)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
@@ -127,7 +131,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -144,7 +148,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -161,7 +165,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -175,7 +179,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -225,7 +229,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -246,7 +250,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -266,7 +270,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -286,7 +290,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -309,7 +313,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -339,7 +343,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -356,7 +360,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -373,7 +377,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -408,7 +412,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -445,7 +449,31 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
run simulator/emulator builds from eas-cli
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios)
|
||||
--id=<value> ID of the simulator/emulator build to run
|
||||
--latest Run the latest simulator/emulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator/emulator build archive or app
|
||||
--url=<value> Simulator/Emulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -465,7 +493,7 @@ FLAGS
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=<value> Path to the .apk/.aab/.ipa file
|
||||
--url=<value> App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--verbose Always print logs from EAS Submit
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
@@ -492,7 +520,7 @@ DESCRIPTION
|
||||
Update version of an app.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -511,7 +539,7 @@ DESCRIPTION
|
||||
Update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -528,7 +556,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -549,7 +577,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -571,7 +599,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -591,7 +619,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -614,7 +642,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -633,7 +661,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -650,7 +678,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -664,7 +692,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -684,7 +712,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -705,7 +733,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -719,7 +747,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -733,7 +761,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -821,7 +849,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -838,7 +866,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -855,7 +883,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas open`
|
||||
|
||||
@@ -869,7 +897,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -883,7 +911,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -905,7 +933,55 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas run`
|
||||
|
||||
run simulator build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the simulator build to run
|
||||
--latest Run the latest simulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator build file file
|
||||
--url=<value> Simulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator build
|
||||
```
|
||||
|
||||
_See code: [src/commands/run/index.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/run/index.ts)_
|
||||
|
||||
## `eas run:run`
|
||||
|
||||
run simulator build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas run:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the simulator build to run
|
||||
--latest Run the latest simulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator build file file
|
||||
--url=<value> Simulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator build
|
||||
```
|
||||
|
||||
_See code: [src/commands/run/run.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/run/run.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -928,7 +1004,7 @@ DESCRIPTION
|
||||
create an environment secret on the current project or owner account
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -946,7 +1022,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -960,7 +1036,7 @@ DESCRIPTION
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas secret:push`
|
||||
|
||||
@@ -980,7 +1056,7 @@ DESCRIPTION
|
||||
read environment secrets from env file and store on the server
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1000,7 +1076,7 @@ FLAGS
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=<value> Path to the .apk/.aab/.ipa file
|
||||
--url=<value> App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--verbose Always print logs from EAS Submit
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
@@ -1010,7 +1086,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1025,7 +1101,7 @@ FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all]
|
||||
--auto Use the current git branch and commit message for the EAS branch and update message
|
||||
--branch=<value> Branch to publish the update group on
|
||||
--group=<value> Update group to republish
|
||||
--group=<value> Update group to republish (deprecated, see republish command)
|
||||
--input-dir=<value> [default: dist] Location of the bundle
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--message=<value> A short message describing the update
|
||||
@@ -1033,14 +1109,14 @@ FLAGS
|
||||
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
|
||||
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
|
||||
certificate's directory.
|
||||
--republish Republish an update group
|
||||
--republish Republish an update group (deprecated, see republish command)
|
||||
--skip-bundler Skip running Expo CLI to bundle the app before publishing
|
||||
|
||||
DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1048,16 +1124,17 @@ configure the project to support EAS Update
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:configure [-p android|ios|all]
|
||||
$ eas update:configure [-p android|ios|all] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all] Platform to configure
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1078,7 +1155,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1100,7 +1177,30 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
rollback to an existing update
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:republish [--branch <value> | --group <value>] [--message <value>] [-p android|ios|all] [--json
|
||||
--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all]
|
||||
--branch=<value> Branch name to select an update to republish from
|
||||
--group=<value> Update group ID to republish
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--message=<value> Short message describing the republished update
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
rollback to an existing update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -1120,7 +1220,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -1141,7 +1241,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -1161,7 +1261,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -1178,7 +1278,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -1200,7 +1300,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1217,7 +1317,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.0.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
|
||||
Generated
+145
@@ -16530,6 +16530,12 @@
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "DEVELOPMENT",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "INTERNAL",
|
||||
"description": "",
|
||||
@@ -17413,6 +17419,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",
|
||||
@@ -17870,6 +17905,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 +17954,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 +18133,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 +18237,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "openGraphImageUrl",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "private",
|
||||
"description": "",
|
||||
@@ -22847,6 +22938,12 @@
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "GCS",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "S3",
|
||||
"description": "",
|
||||
@@ -23176,6 +23273,26 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "excludedAssets",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "PartialManifestAsset",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "gitCommitHash",
|
||||
"description": "",
|
||||
@@ -23188,6 +23305,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "isGitWorkingTreeDirty",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"description": "",
|
||||
@@ -27568,6 +27697,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.0.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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -160,7 +160,7 @@ export async function ensureBranchExistsAsync(
|
||||
appId: string;
|
||||
branchName: string;
|
||||
}
|
||||
): Promise<{ branchId: string }> {
|
||||
): Promise<{ branchId: string; createdBranch: boolean }> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
@@ -168,14 +168,14 @@ export async function ensureBranchExistsAsync(
|
||||
});
|
||||
|
||||
const { id } = updateBranch;
|
||||
return { branchId: id };
|
||||
return { branchId: id, createdBranch: false };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id };
|
||||
return { branchId: newUpdateBranch.id, createdBranch: true };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,11 +2,11 @@ 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 { formatGraphQLBuild } from './utils/formatBuild';
|
||||
@@ -55,18 +55,51 @@ export async function listAndRenderBuildsOnAppAsync(
|
||||
}
|
||||
}
|
||||
|
||||
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 +115,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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':
|
||||
|
||||
@@ -34,6 +34,7 @@ export default class BuildList extends EasCommand {
|
||||
}),
|
||||
distribution: Flags.enum({
|
||||
options: [
|
||||
BuildDistributionType.DEVELOPMENT,
|
||||
BuildDistributionType.STORE,
|
||||
BuildDistributionType.INTERNAL,
|
||||
BuildDistributionType.SIMULATOR,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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';
|
||||
@@ -17,28 +17,27 @@ import fetch from '../../fetch';
|
||||
import {
|
||||
PublishUpdateGroupInput,
|
||||
StatuspageServiceName,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
UpdatePublishMutation,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
ExpoCLIExportPlatformFlag,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
filterExportedPlatformsByFlag,
|
||||
isUploadedAssetCountAboveWarningThreshold,
|
||||
resolveInputDirectoryAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
|
||||
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
|
||||
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
@@ -46,34 +45,35 @@ import {
|
||||
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';
|
||||
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
|
||||
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
message?: string;
|
||||
group?: string;
|
||||
republish?: boolean;
|
||||
platform: string;
|
||||
'input-dir': string;
|
||||
'skip-bundler': boolean;
|
||||
'private-key-path'?: string;
|
||||
'non-interactive': boolean;
|
||||
json: boolean;
|
||||
/** @deprecated see UpdateRepublish command */
|
||||
group?: string;
|
||||
/** @deprecated see UpdateRepublish command */
|
||||
republish?: boolean;
|
||||
};
|
||||
|
||||
type UpdateFlags = {
|
||||
auto: boolean;
|
||||
platform: RequestedPlatform;
|
||||
platform: ExpoCLIExportPlatformFlag;
|
||||
branchName?: string;
|
||||
updateMessage?: string;
|
||||
republish: boolean;
|
||||
groupId?: string;
|
||||
inputDir: string;
|
||||
skipBundler: boolean;
|
||||
privateKeyPath?: string;
|
||||
@@ -81,6 +81,21 @@ type UpdateFlags = {
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
static override description = 'publish an update group';
|
||||
|
||||
@@ -94,11 +109,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
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({
|
||||
@@ -112,7 +127,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,
|
||||
}),
|
||||
@@ -141,8 +160,6 @@ export default class UpdatePublish extends EasCommand {
|
||||
platform: platformFlag,
|
||||
branchName,
|
||||
updateMessage,
|
||||
republish,
|
||||
groupId,
|
||||
inputDir,
|
||||
skipBundler,
|
||||
privateKeyPath,
|
||||
@@ -177,14 +194,15 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
platform: platformFlag,
|
||||
platform: getRequestedPlatform(platformFlag),
|
||||
projectDir,
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync();
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
|
||||
if (!branchName) {
|
||||
if (autoFlag) {
|
||||
@@ -195,9 +213,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
try {
|
||||
const branch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like to ${
|
||||
republish ? 'republish' : 'publish'
|
||||
} on?`,
|
||||
promptTitle: `Which branch would you like to publish on?`,
|
||||
displayTextForListItem: updateBranch =>
|
||||
`${updateBranch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
|
||||
@@ -220,148 +236,72 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateMessage && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
const bundleSpinner = ora().start('Exporting...');
|
||||
try {
|
||||
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
|
||||
bundleSpinner.succeed('Exported bundle(s)');
|
||||
} catch (e) {
|
||||
bundleSpinner.fail('Export failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// After possibly bundling, assert that the input directory can be found.
|
||||
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
|
||||
|
||||
const assetSpinner = ora().start('Uploading...');
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let oldMessage: string, oldRuntimeVersion: string;
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
|
||||
if (republish) {
|
||||
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
|
||||
// Instead we get the `updateInfoGroup` from the update we wish to republish.
|
||||
let updatesToRepublish: Pick<
|
||||
Update,
|
||||
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
|
||||
>[];
|
||||
if (groupId) {
|
||||
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
|
||||
groupId,
|
||||
});
|
||||
updatesToRepublish = updatesByGroup;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --group when in non-interactive mode');
|
||||
}
|
||||
try {
|
||||
const collectedAssets = await collectAssetsAsync(distRoot);
|
||||
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
|
||||
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
|
||||
|
||||
updatesToRepublish = await selectUpdateGroupOnBranchAsync(graphqlClient, {
|
||||
projectId,
|
||||
branchName,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
}
|
||||
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
|
||||
// Only republish to the specified platforms
|
||||
update => platformFlag === 'all' || update.platform === platformFlag
|
||||
const uploadResults = await uploadAssetsAsync(
|
||||
graphqlClient,
|
||||
assets,
|
||||
projectId,
|
||||
(totalAssets, missingAssets) => {
|
||||
assetSpinner.text = `Uploading (${totalAssets - missingAssets}/${totalAssets})`;
|
||||
}
|
||||
);
|
||||
if (updatesToRepublishFilteredByPlatform.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
|
||||
groupId ? groupId : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
let publicationPlatformMessage: string;
|
||||
if (platformFlag === 'all') {
|
||||
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
|
||||
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
|
||||
}
|
||||
publicationPlatformMessage = `The republished update will appear on the same platforms it was originally published on: ${updatesToRepublishFilteredByPlatform
|
||||
.map(update => update.platform)
|
||||
.join(', ')}`;
|
||||
} else {
|
||||
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
|
||||
}
|
||||
Log.withTick(publicationPlatformMessage);
|
||||
|
||||
for (const update of updatesToRepublishFilteredByPlatform) {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
groupId = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: `Republish "${oldMessage!}" - group: ${groupId}`,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
if (!updateMessage && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
const bundleSpinner = ora().start('Building bundle...');
|
||||
try {
|
||||
await buildBundlesAsync({ projectDir, inputDir, exp });
|
||||
bundleSpinner.succeed('Built bundle!');
|
||||
} catch (e) {
|
||||
bundleSpinner.fail('Failed to build bundle!');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const assetSpinner = ora().start('Uploading assets...');
|
||||
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);
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
throw e;
|
||||
}
|
||||
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
|
||||
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
const uploadAssetSuccessMessage = uploadedAssetCount
|
||||
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
|
||||
: `Uploaded: No changes detected`;
|
||||
assetSpinner.succeed(uploadAssetSuccessMessage);
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload');
|
||||
throw e;
|
||||
}
|
||||
|
||||
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
|
||||
@@ -369,29 +309,40 @@ export default class UpdatePublish extends EasCommand {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
}
|
||||
|
||||
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 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 } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchName,
|
||||
});
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchId,
|
||||
channelName: branchName,
|
||||
});
|
||||
if (createdBranch) {
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchId,
|
||||
channelName: branchName,
|
||||
});
|
||||
}
|
||||
|
||||
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
|
||||
|
||||
const gitCommitHash = await getVcsClient().getCommitHashAsync();
|
||||
const vcsClient = getVcsClient();
|
||||
|
||||
const gitCommitHash = await vcsClient.getCommitHashAsync();
|
||||
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
|
||||
([runtime, platforms]) => {
|
||||
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
|
||||
({ runtimeVersion, platforms }) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
@@ -399,17 +350,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,
|
||||
};
|
||||
}
|
||||
@@ -478,12 +425,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(
|
||||
@@ -500,7 +450,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
|
||||
@@ -508,7 +458,14 @@ export default class UpdatePublish extends EasCommand {
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: truncatedMessage },
|
||||
...(gitCommitHash ? [{ label: 'Commit', value: gitCommitHash }] : []),
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
label: 'Commit',
|
||||
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Website link', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
@@ -540,18 +497,27 @@ export default class UpdatePublish extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
const groupId = flags.group;
|
||||
const republish = flags.republish || !!groupId; // When --group is defined, we are republishing
|
||||
if (nonInteractive && republish && !groupId) {
|
||||
Errors.error(`--group is required when updating in non-interactive mode`, { exit: 1 });
|
||||
if (flags.group || flags.republish) {
|
||||
// Pick the first flag set that is defined, in this specific order
|
||||
const args = [
|
||||
['--group', flags.group],
|
||||
['--branch', flags.branch],
|
||||
].filter(([_, value]) => value)[0];
|
||||
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
'The --group and --republish flags are deprecated, use the republish command instead:'
|
||||
);
|
||||
Log.warn(` ${chalk.bold([`eas update:republish`, ...(args ?? [])].join(' '))}`);
|
||||
Log.newLine();
|
||||
|
||||
Errors.error('--group and --republish flags are deprecated', { exit: 1 });
|
||||
}
|
||||
|
||||
return {
|
||||
auto,
|
||||
branchName,
|
||||
updateMessage,
|
||||
groupId,
|
||||
republish,
|
||||
inputDir: flags['input-dir'],
|
||||
skipBundler: flags['skip-bundler'],
|
||||
platform: flags.platform as RequestedPlatform,
|
||||
@@ -562,31 +528,20 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
function transformRuntimeVersions(exp: ExpoConfig, platforms: Platform[]): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/** 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,
|
||||
platform: RequestedPlatform,
|
||||
platforms: PublishPlatform[],
|
||||
projectDir: string
|
||||
): Promise<Record<string, string>> {
|
||||
const platforms = (platform === 'all' ? ['android', 'ios'] : [platform]) 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.'
|
||||
@@ -595,5 +550,18 @@ async function getRuntimeVersionObjectAsync(
|
||||
}
|
||||
}
|
||||
|
||||
return transformRuntimeVersions(exp, platforms);
|
||||
return [...new Set(platforms)].map(platform => {
|
||||
if (platform === 'web') {
|
||||
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
|
||||
}
|
||||
return {
|
||||
platform,
|
||||
runtimeVersion: nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { ApplePlatform } from './constants';
|
||||
import { getCertificateBySerialNumberAsync, transformCertificate } from './distributionCertificate';
|
||||
|
||||
export enum ProfileClass {
|
||||
Development = 'development',
|
||||
Adhoc = 'ad_hoc',
|
||||
General = 'general',
|
||||
}
|
||||
@@ -38,26 +39,29 @@ function resolveProfileTypeAppleTv(
|
||||
profileClass: ProfileClass,
|
||||
isEnterprise?: boolean
|
||||
): ProfileType {
|
||||
if (profileClass === ProfileClass.Development) {
|
||||
return ProfileType.TVOS_APP_DEVELOPMENT;
|
||||
} else if (profileClass === ProfileClass.Adhoc) {
|
||||
return ProfileType.TVOS_APP_ADHOC;
|
||||
}
|
||||
if (isEnterprise) {
|
||||
return profileClass === ProfileClass.Adhoc
|
||||
? ProfileType.TVOS_APP_ADHOC
|
||||
: ProfileType.TVOS_APP_INHOUSE;
|
||||
return ProfileType.TVOS_APP_INHOUSE;
|
||||
} else {
|
||||
return profileClass === ProfileClass.Adhoc
|
||||
? ProfileType.TVOS_APP_ADHOC
|
||||
: ProfileType.TVOS_APP_STORE;
|
||||
return ProfileType.TVOS_APP_STORE;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProfileTypeIos(profileClass: ProfileClass, isEnterprise?: boolean): ProfileType {
|
||||
if (profileClass === ProfileClass.Development) {
|
||||
return ProfileType.IOS_APP_DEVELOPMENT;
|
||||
} else if (profileClass === ProfileClass.Adhoc) {
|
||||
return ProfileType.IOS_APP_ADHOC;
|
||||
}
|
||||
|
||||
if (isEnterprise) {
|
||||
return profileClass === ProfileClass.Adhoc
|
||||
? ProfileType.IOS_APP_ADHOC
|
||||
: ProfileType.IOS_APP_INHOUSE;
|
||||
return ProfileType.IOS_APP_INHOUSE;
|
||||
} else {
|
||||
return profileClass === ProfileClass.Adhoc
|
||||
? ProfileType.IOS_APP_ADHOC
|
||||
: ProfileType.IOS_APP_STORE;
|
||||
return ProfileType.IOS_APP_STORE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +229,7 @@ export async function createProvisioningProfileAsync(
|
||||
const profile = await Profile.createAsync(context, {
|
||||
bundleId: bundleIdItem.id,
|
||||
name: profileName,
|
||||
// TODO: Development should probably contain multiple certs.
|
||||
certificates: [certificate.id],
|
||||
devices: [],
|
||||
profileType,
|
||||
|
||||
@@ -100,6 +100,8 @@ async function validateProvisioningProfileWithAppleAsync(
|
||||
applePlatform,
|
||||
buildCredentials.iosDistributionType === IosDistributionType.AdHoc
|
||||
? ProfileClass.Adhoc
|
||||
: buildCredentials.iosDistributionType === IosDistributionType.Development
|
||||
? ProfileClass.Development
|
||||
: ProfileClass.General
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+32
-16
@@ -2523,6 +2523,12 @@ export type GetSignedAssetUploadSpecificationsResult = {
|
||||
specifications: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum GitHubAppEnvironment {
|
||||
Development = 'DEVELOPMENT',
|
||||
Production = 'PRODUCTION',
|
||||
Staging = 'STAGING'
|
||||
}
|
||||
|
||||
export type GitHubAppInstallation = {
|
||||
__typename?: 'GitHubAppInstallation';
|
||||
accessibleRepositories: GitHubRepositoryPaginationResult;
|
||||
@@ -2586,7 +2592,9 @@ export type GitHubAppQuery = {
|
||||
__typename?: 'GitHubAppQuery';
|
||||
appIdentifier: Scalars['String'];
|
||||
clientIdentifier: Scalars['String'];
|
||||
environment: GitHubAppEnvironment;
|
||||
installation: GitHubAppInstallation;
|
||||
name: Scalars['String'];
|
||||
searchRepositories: GitHubRepositoryPaginationResult;
|
||||
};
|
||||
|
||||
@@ -2613,12 +2621,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'];
|
||||
};
|
||||
|
||||
@@ -3287,6 +3297,7 @@ export type ProjectArchiveSourceInput = {
|
||||
};
|
||||
|
||||
export enum ProjectArchiveSourceType {
|
||||
Gcs = 'GCS',
|
||||
S3 = 'S3',
|
||||
Url = 'URL'
|
||||
}
|
||||
@@ -3336,7 +3347,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;
|
||||
@@ -3996,6 +4009,7 @@ export type Update = ActivityTimelineProjectActivity & {
|
||||
gitCommitHash?: Maybe<Scalars['String']>;
|
||||
group: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
isGitWorkingTreeDirty: Scalars['Boolean'];
|
||||
manifestFragment: Scalars['String'];
|
||||
manifestPermalink: Scalars['String'];
|
||||
message?: Maybe<Scalars['String']>;
|
||||
@@ -5006,7 +5020,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'];
|
||||
@@ -5016,7 +5030,7 @@ 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 CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
@@ -5072,6 +5086,7 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
config: AndroidSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']>;
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -5081,6 +5096,7 @@ export type CreateIosSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
config: IosSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']>;
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -5154,7 +5170,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'];
|
||||
@@ -5164,21 +5180,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'];
|
||||
@@ -5188,7 +5204,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'];
|
||||
@@ -5196,7 +5212,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'];
|
||||
@@ -5205,7 +5221,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'];
|
||||
@@ -5258,7 +5274,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'];
|
||||
@@ -5269,7 +5285,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'];
|
||||
@@ -5279,7 +5295,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; }>;
|
||||
|
||||
@@ -5305,9 +5321,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 };
|
||||
|
||||
@@ -5315,9 +5331,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 };
|
||||
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 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] = {
|
||||
@@ -149,10 +151,12 @@ 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) {
|
||||
@@ -160,7 +164,14 @@ export async function buildBundlesAsync({
|
||||
}
|
||||
|
||||
if (shouldUseVersionedExpoCLI(projectDir, exp)) {
|
||||
await expoCommandAsync(projectDir, ['export', '--output-dir', inputDir, '--dump-sourcemap']);
|
||||
await expoCommandAsync(projectDir, [
|
||||
'export',
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--dump-sourcemap',
|
||||
'--platform',
|
||||
platformFlag,
|
||||
]);
|
||||
} else {
|
||||
// Legacy global Expo CLI
|
||||
await expoCommandAsync(projectDir, [
|
||||
@@ -170,18 +181,23 @@ export async function buildBundlesAsync({
|
||||
'--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;
|
||||
}
|
||||
@@ -200,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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -272,7 +272,7 @@ export async function ensureEASUpdatesIsConfiguredAsync(
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
platform: RequestedPlatform;
|
||||
platform: RequestedPlatform | null;
|
||||
}
|
||||
): Promise<void> {
|
||||
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(
|
||||
@@ -284,6 +284,12 @@ export async function ensureEASUpdatesIsConfiguredAsync(
|
||||
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({
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -10,6 +10,8 @@ export const buildDistributionTypeToGraphQLDistributionType = (
|
||||
return DistributionType.Internal;
|
||||
} else if (buildDistribution === BuildDistributionType.SIMULATOR) {
|
||||
return DistributionType.Simulator;
|
||||
} else if (buildDistribution === BuildDistributionType.DEVELOPMENT) {
|
||||
return DistributionType.Development;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -23,7 +23,10 @@ function dotExpoHomeDirectory(): string {
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
const getStateJsonPath = (): string => path.join(dotExpoHomeDirectory(), 'state.json');
|
||||
export const getStateJsonPath = (): string => path.join(dotExpoHomeDirectory(), 'state.json');
|
||||
|
||||
export const getEasBuildRunCacheDirectoryPath = (): string =>
|
||||
path.join(getTmpDirectory(), 'eas-build-run-cache');
|
||||
|
||||
// Paths for storing things like data, config, cache, etc.
|
||||
// Should use the correct OS-specific paths (e.g. XDG base directory on Linux)
|
||||
@@ -35,17 +38,8 @@ const {
|
||||
temp: TEMP_PATH,
|
||||
} = envPaths('eas-cli');
|
||||
|
||||
const getDataDirectory = (): string => DATA_PATH;
|
||||
const getConfigDirectory = (): string => CONFIG_PATH;
|
||||
const getCacheDirectory = (): string => CACHE_PATH;
|
||||
const getLogDirectory = (): string => LOG_PATH;
|
||||
const getTmpDirectory = (): string => TEMP_PATH;
|
||||
|
||||
export {
|
||||
getDataDirectory,
|
||||
getConfigDirectory,
|
||||
getCacheDirectory,
|
||||
getLogDirectory,
|
||||
getTmpDirectory,
|
||||
getStateJsonPath,
|
||||
};
|
||||
export const getDataDirectory = (): string => DATA_PATH;
|
||||
export const getConfigDirectory = (): string => CONFIG_PATH;
|
||||
export const getCacheDirectory = (): string => CACHE_PATH;
|
||||
export const getLogDirectory = (): string => LOG_PATH;
|
||||
export const getTmpDirectory = (): string => TEMP_PATH;
|
||||
@@ -21,7 +21,7 @@ export function createProgressTracker({
|
||||
completedMessage,
|
||||
}: {
|
||||
total?: number;
|
||||
message: string | ((ratio: number) => string);
|
||||
message: string | ((ratio: number, total: number) => string);
|
||||
completedMessage: string | ((duration: string) => string);
|
||||
}): ProgressHandler {
|
||||
let bar: Ora | null = null;
|
||||
@@ -31,17 +31,19 @@ export function createProgressTracker({
|
||||
|
||||
const timerLabel = String(Date.now());
|
||||
|
||||
const getMessage = (v: number): string => {
|
||||
const getMessage = (v: number, total: number): string => {
|
||||
const ratio = Math.min(Math.max(v, 0), 1);
|
||||
const percent = Math.floor(ratio * 100);
|
||||
return typeof message === 'string' ? `${message} ${percent.toFixed(0)}%` : message(ratio);
|
||||
return typeof message === 'string'
|
||||
? `${message} ${percent.toFixed(0)}%`
|
||||
: message(ratio, total);
|
||||
};
|
||||
|
||||
return ({ progress, isComplete, error }) => {
|
||||
if (progress) {
|
||||
if (!bar && (progress.total !== undefined || total !== undefined)) {
|
||||
calcTotal = (total ?? progress.total) as number;
|
||||
bar = ora(getMessage(0)).start();
|
||||
bar = ora(getMessage(0, calcTotal)).start();
|
||||
startTimer(timerLabel);
|
||||
}
|
||||
if (progress.total) {
|
||||
@@ -56,7 +58,7 @@ export function createProgressTracker({
|
||||
percentage = current / calcTotal;
|
||||
}
|
||||
|
||||
bar.text = getMessage(percentage);
|
||||
bar.text = getMessage(percentage, calcTotal);
|
||||
}
|
||||
transferredSoFar = progress.transferred;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.18.6",
|
||||
"@expo/eas-build-job": "0.2.96",
|
||||
"@expo/eas-build-job": "0.2.97",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
@@ -14,6 +14,7 @@
|
||||
"joi": "17.6.4",
|
||||
"log-symbols": "4.1.0",
|
||||
"semver": "7.3.8",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"markdownDescription": "The channel is a name we can give to multiple builds to identify them easily. [Learn more](https://docs.expo.dev/eas-update/how-eas-update-works/)\n\n**This field only applies to the EAS Update service**, if your project still uses Classic Updates then use the [releaseChannel](https://docs.expo.dev/build-reference/eas-json/#releasechannel) field instead."
|
||||
},
|
||||
"distribution": {
|
||||
"enum": ["store", "internal"],
|
||||
"enum": ["internal", "store", "development"],
|
||||
"description": "The method of distributing your app. Learn more: https://docs.expo.dev/build/internal-distribution/",
|
||||
"markdownDescription": "The method of distributing your app.\n\n- `internal` - with this option you'll be able to share your build URLs with anyone, and they will be able to install the builds to their devices straight from the Expo website. When using `internal`, make sure the build produces an APK or IPA file. Otherwise, the shareable URL will be useless. [Learn more](https://docs.expo.dev/build/internal-distribution/)\n- `store` - produces builds for store uploads, your build URLs won't be shareable.",
|
||||
"markdownEnumDescriptions": [
|
||||
@@ -303,6 +303,7 @@
|
||||
"enum": [
|
||||
"default",
|
||||
"latest",
|
||||
"macos-monterey-12.6-xcode-14.1",
|
||||
"macos-monterey-12.6-xcode-14.0",
|
||||
"macos-monterey-12.4-xcode-13.4",
|
||||
"macos-monterey-12.3-xcode-13.3",
|
||||
@@ -310,10 +311,7 @@
|
||||
"macos-big-sur-11.4-xcode-13.0",
|
||||
"macos-big-sur-11.4-xcode-12.5"
|
||||
],
|
||||
"markdownEnumDescriptions": [
|
||||
"`macos-monterey-12.4-xcode-13.4`",
|
||||
"`macos-monterey-12.6-xcode-14.0`"
|
||||
]
|
||||
"markdownEnumDescriptions": ["`macos-monterey-12.6-xcode-14.0`", "`macos-monterey-12.6-xcode-14.1`"]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
|
||||
@@ -3,12 +3,40 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import * as fleece from 'golden-fleece';
|
||||
import { ValidationError } from 'joi';
|
||||
import path from 'path';
|
||||
|
||||
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
|
||||
import { link } from './log';
|
||||
import { EasJsonSchema } from './schema';
|
||||
import { EasJson } from './types';
|
||||
|
||||
const customErrorMessageHandlers: ((err: ValidationError) => void)[] = [
|
||||
// Ask user to upgrade eas-cli version or check the docs when image is invalid.
|
||||
(err: ValidationError) => {
|
||||
for (const detail of err.details) {
|
||||
// image should be only placed under 'build.profilename.platform.image' key
|
||||
// if it's not the case show standard Joi error
|
||||
if (
|
||||
detail.path.length === 4 &&
|
||||
detail.path[0] === 'build' &&
|
||||
['ios', 'android'].includes(detail.path[2].toString()) &&
|
||||
detail.path[3] === 'image'
|
||||
) {
|
||||
throw new InvalidEasJsonError(
|
||||
chalk.red(
|
||||
`Specified build image '${
|
||||
detail?.context?.value
|
||||
}' is not recognized. Please update your EAS CLI and see ${link(
|
||||
'https://docs.expo.dev/build-reference/infrastructure/'
|
||||
)} for supported build images.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export class EasJsonAccessor {
|
||||
private easJsonPath: string;
|
||||
|
||||
@@ -39,6 +67,10 @@ export class EasJsonAccessor {
|
||||
noDefaults: true,
|
||||
});
|
||||
if (error) {
|
||||
for (const handler of customErrorMessageHandlers) {
|
||||
handler(error);
|
||||
}
|
||||
|
||||
const errorMessages = error.message.split('. ');
|
||||
throw new InvalidEasJsonError(
|
||||
`${chalk.bold('eas.json')} is not valid.\n- ${errorMessages.join('\n- ')}`
|
||||
|
||||
@@ -11,7 +11,7 @@ const CacheSchema = Joi.object({
|
||||
|
||||
const CommonBuildProfileSchema = Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
|
||||
distribution: Joi.string().valid('store', 'internal').default('store'),
|
||||
distribution: Joi.string().valid('store', 'development', 'internal').default('store'),
|
||||
cache: CacheSchema,
|
||||
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
|
||||
@@ -5,7 +5,7 @@ export enum CredentialsSource {
|
||||
REMOTE = 'remote',
|
||||
}
|
||||
|
||||
export type DistributionType = 'store' | 'internal';
|
||||
export type DistributionType = 'store' | 'internal' | 'development';
|
||||
|
||||
export type IosEnterpriseProvisioning = 'adhoc' | 'universal';
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import chalk from 'chalk';
|
||||
import terminalLink from 'terminal-link';
|
||||
|
||||
// link function from packages/eas-cli/src/log.ts
|
||||
export function link(
|
||||
url: string,
|
||||
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
|
||||
): string {
|
||||
let output: string;
|
||||
if (terminalLink.isSupported) {
|
||||
output = terminalLink(text, url);
|
||||
} else {
|
||||
output = `${text === url ? '' : text + ': '}${chalk.underline(url)}`;
|
||||
}
|
||||
return dim ? chalk.dim(output) : output;
|
||||
}
|
||||
@@ -5,5 +5,8 @@ set -eo pipefail
|
||||
SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
pushd $SCRIPTS_DIR >/dev/null 2>&1
|
||||
# Install modules
|
||||
yarn --silent
|
||||
# Run script
|
||||
yarn --silent $@
|
||||
popd >/dev/null 2>&1
|
||||
@@ -1378,10 +1378,10 @@
|
||||
slugify "^1.3.4"
|
||||
sucrase "^3.20.0"
|
||||
|
||||
"@expo/[email protected]6":
|
||||
version "0.2.96"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.96.tgz#711d0a03e078d7e03e9514a5c7acdea0d593d110"
|
||||
integrity sha512-Y8jeEEwzU/GfYNSq+5Snbl5NFpXSgqtRCvPIpWUWka0XsPf4qhU9e6iWjOJboi5VbQIAQfZbXMjW/OZByjT/OA==
|
||||
"@expo/[email protected]7":
|
||||
version "0.2.97"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.97.tgz#8f0884c498c17bb3d3e19cad767ee7f3b6b81c05"
|
||||
integrity sha512-1OtHIoJbMQSsp0jEqBLI7nsqqhy1Hv38H0Ogznyr3uFqk5zWLX57pmSx4wWYv9R8M6SXsgJJq/nNa5RAopsipA==
|
||||
dependencies:
|
||||
joi "^17.4.2"
|
||||
semver "^7.3.5"
|
||||
|
||||
Reference in new issue
Block a user