Compare commits

..
Author SHA1 Message Date
Dominik Sokal 9b711eae77 address PR feedback 2022-11-04 12:16:22 +01:00
Dominik Sokal 2bca449793 set EAS Update default channel to the build profile name 2022-11-04 10:42:15 +01:00
58 changed files with 731 additions and 1018 deletions

No files matched your search

-28
View File
@@ -8,43 +8,15 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
### 🧹 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))
## [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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.7.1"
"version": "2.6.0"
}
+1 -1
View File
@@ -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 --stream --parallel",
"start": "lerna run watch --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
+50 -101
View File
@@ -95,8 +95,6 @@ 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)
@@ -129,7 +127,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/login.ts)_
_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)_
## `eas account:logout`
@@ -146,7 +144,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/logout.ts)_
_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)_
## `eas account:view`
@@ -163,7 +161,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/account/view.ts)_
_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)_
## `eas analytics [STATUS]`
@@ -177,7 +175,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -227,7 +225,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/create.ts)_
_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)_
## `eas branch:delete [NAME]`
@@ -248,7 +246,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/delete.ts)_
_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)_
## `eas branch:list`
@@ -268,7 +266,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/list.ts)_
_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)_
## `eas branch:rename`
@@ -288,7 +286,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/rename.ts)_
_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)_
## `eas branch:view [NAME]`
@@ -311,7 +309,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/branch/view.ts)_
_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)_
## `eas build`
@@ -341,7 +339,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/index.ts)_
_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)_
## `eas build:cancel [BUILD_ID]`
@@ -358,7 +356,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/cancel.ts)_
_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)_
## `eas build:configure`
@@ -375,7 +373,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/configure.ts)_
_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)_
## `eas build:inspect`
@@ -410,7 +408,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.7.1/packages/eas-cli/src/commands/build/inspect.ts)_
_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)_
## `eas build:list`
@@ -447,7 +445,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/list.ts)_
_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)_
## `eas build:submit`
@@ -494,7 +492,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/version/set.ts)_
_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)_
## `eas build:version:sync`
@@ -513,7 +511,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.7.1/packages/eas-cli/src/commands/build/version/sync.ts)_
_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)_
## `eas build:view [BUILD_ID]`
@@ -530,7 +528,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/build/view.ts)_
_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)_
## `eas channel:create [NAME]`
@@ -551,7 +549,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/create.ts)_
_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)_
## `eas channel:edit [NAME]`
@@ -573,7 +571,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/edit.ts)_
_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)_
## `eas channel:list`
@@ -593,7 +591,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/list.ts)_
_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)_
## `eas channel:view [NAME]`
@@ -616,7 +614,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/channel/view.ts)_
_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)_
## `eas config`
@@ -635,7 +633,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -652,7 +650,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -666,7 +664,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.7.1/packages/eas-cli/src/commands/device/create.ts)_
_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)_
## `eas device:delete`
@@ -686,7 +684,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/delete.ts)_
_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)_
## `eas device:list`
@@ -707,7 +705,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/list.ts)_
_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)_
## `eas device:view [UDID]`
@@ -721,7 +719,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/device/view.ts)_
_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)_
## `eas diagnostics`
@@ -735,7 +733,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -823,7 +821,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/metadata/lint.ts)_
_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)_
## `eas metadata:pull`
@@ -840,7 +838,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.7.1/packages/eas-cli/src/commands/metadata/pull.ts)_
_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)_
## `eas metadata:push`
@@ -857,7 +855,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.7.1/packages/eas-cli/src/commands/metadata/push.ts)_
_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)_
## `eas open`
@@ -871,7 +869,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -885,7 +883,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/project/info.ts)_
_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)_
## `eas project:init`
@@ -907,55 +905,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.7.1/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/v2.7.1/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/v2.7.1/packages/eas-cli/src/commands/run/run.ts)_
_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)_
## `eas secret:create`
@@ -978,7 +928,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.7.1/packages/eas-cli/src/commands/secret/create.ts)_
_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)_
## `eas secret:delete`
@@ -996,7 +946,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/secret/delete.ts)_
_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)_
## `eas secret:list`
@@ -1010,7 +960,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.7.1/packages/eas-cli/src/commands/secret/list.ts)_
_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)_
## `eas secret:push`
@@ -1030,7 +980,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.7.1/packages/eas-cli/src/commands/secret/push.ts)_
_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)_
## `eas submit`
@@ -1060,7 +1010,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1090,7 +1040,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/index.ts)_
_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)_
## `eas update:configure`
@@ -1098,17 +1048,16 @@ configure the project to support EAS Update
```
USAGE
$ eas update:configure [-p android|ios|all] [--non-interactive]
$ eas update:configure [-p android|ios|all]
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.7.1/packages/eas-cli/src/commands/update/configure.ts)_
_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)_
## `eas update:delete GROUPID`
@@ -1129,7 +1078,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.7.1/packages/eas-cli/src/commands/update/delete.ts)_
_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)_
## `eas update:list`
@@ -1151,7 +1100,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/list.ts)_
_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)_
## `eas update:view GROUPID`
@@ -1171,7 +1120,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/update/view.ts)_
_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)_
## `eas webhook:create`
@@ -1192,7 +1141,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/create.ts)_
_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)_
## `eas webhook:delete [ID]`
@@ -1212,7 +1161,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/delete.ts)_
_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)_
## `eas webhook:list`
@@ -1229,7 +1178,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/list.ts)_
_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)_
## `eas webhook:update`
@@ -1251,7 +1200,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/update.ts)_
_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)_
## `eas webhook:view ID`
@@ -1268,7 +1217,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.7.1/packages/eas-cli/src/commands/webhook/view.ts)_
_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)_
## `eas whoami`
-91
View File
@@ -17413,35 +17413,6 @@
"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",
@@ -17899,22 +17870,6 @@
"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": "",
@@ -17948,22 +17903,6 @@
"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": "",
@@ -18127,18 +18066,6 @@
"name": "GitHubRepositoryMetadata",
"description": "",
"fields": [
{
"name": "defaultBranch",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "githubRepoDescription",
"description": "",
@@ -18231,18 +18158,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "openGraphImageUrl",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "private",
"description": "",
@@ -22932,12 +22847,6 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "GCS",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "S3",
"description": "",
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "2.7.1",
"version": "2.6.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.97",
"@expo/eas-json": "2.7.1",
"@expo/eas-build-job": "0.2.96",
"@expo/eas-json": "2.6.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
@@ -0,0 +1,17 @@
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);
});
});
@@ -7,6 +7,7 @@ import { AndroidCredentials } from '../../credentials/android/AndroidCredentials
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
import { resolveChannel } from '../utils/updates';
interface JobData {
projectArchive: ArchiveSource;
@@ -68,7 +69,7 @@ export async function prepareJobAsync(
...buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
updates: { channel: resolveChannel(ctx) },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
+4 -4
View File
@@ -26,7 +26,7 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { uploadFileAtPathToS3Async } 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.GCS,
type: ArchiveSourceType.S3,
bucketKey: await uploadProjectAsync(ctx),
} as const);
@@ -219,9 +219,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
projectTarballPath = projectTarball.path;
const bucketKey = await uploadFileAtPathToGCSAsync(
const { bucketKey } = await uploadFileAtPathToS3Async(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectSources,
UploadSessionType.EasBuildProjectSources,
projectTarball.path,
createProgressTracker({
total: projectTarball.size,
@@ -78,7 +78,6 @@ 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,
-5
View File
@@ -16,11 +16,6 @@ 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,
+2 -1
View File
@@ -6,6 +6,7 @@ import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
import { resolveChannel } from '../utils/updates';
interface JobData {
projectArchive: ArchiveSource;
@@ -58,7 +59,7 @@ export async function prepareJobAsync(
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
updates: { channel: resolveChannel(ctx) },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.119';
const PLUGIN_PACKAGE_VERSION = '0.0.115';
export interface LocalBuildOptions {
enable: boolean;
+13 -7
View File
@@ -5,7 +5,11 @@ import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
import Log from '../log';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import {
getUsername,
isEASUpdateConfigured,
isExpoUpdatesInstalled,
} from '../project/projectUtils';
import {
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
@@ -19,6 +23,7 @@ import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { resolveChannel } from './utils/updates';
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
@@ -111,16 +116,17 @@ async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
if (ctx.buildProfile.channel) {
return { channel: ctx.buildProfile.channel };
const maybeChannel = resolveChannel(ctx);
if (maybeChannel) {
return { channel: maybeChannel };
}
const nativeChannel = await getNativeChannelAsync(ctx);
if (isEASUpdateConfigured(ctx.projectDir, ctx.exp) && nativeChannel) {
return { channel: nativeChannel };
}
if (ctx.buildProfile.releaseChannel) {
return { releaseChannel: ctx.buildProfile.releaseChannel };
}
const channel = await getNativeChannelAsync(ctx);
if (channel) {
return { channel };
}
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
return { releaseChannel };
}
@@ -0,0 +1,72 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { instance, mock, when } from 'ts-mockito';
import { isEASUpdateConfigured } from '../../../project/projectUtils';
import { BuildContext } from '../../context';
import { resolveChannel } from '../updates';
jest.mock('../../../project/projectUtils', () => ({
isEASUpdateConfigured: jest.fn(),
}));
describe(resolveChannel, () => {
it('returns "channel" from build profile if specified', () => {
jest.mocked(isEASUpdateConfigured).mockReturnValue(true);
const buildProfileMock = mock<BuildProfile<Platform>>();
when(buildProfileMock.channel).thenReturn('blahblah');
const buildProfile = instance(buildProfileMock);
const ctxMock = mock<BuildContext<Platform>>();
when(ctxMock.exp).thenReturn({
name: 'todo',
slug: 'todo',
updates: { url: 'http://sokal.dev/todo' },
});
when(ctxMock.buildProfileName).thenReturn('production');
when(ctxMock.buildProfile).thenReturn(buildProfile);
const ctx = instance(ctxMock);
expect(resolveChannel(ctx)).toBe('blahblah');
});
it('returns undefined if "channel" is not specified in build profile and EAS Update is not configured', () => {
jest.mocked(isEASUpdateConfigured).mockReturnValue(false);
const buildProfileMock = mock<BuildProfile<Platform>>();
when(buildProfileMock.channel).thenReturn(undefined);
const buildProfile = instance(buildProfileMock);
const ctxMock = mock<BuildContext<Platform>>();
when(ctxMock.exp).thenReturn({
name: 'todo',
slug: 'todo',
});
when(ctxMock.buildProfileName).thenReturn('production');
when(ctxMock.buildProfile).thenReturn(buildProfile);
const ctx = instance(ctxMock);
expect(resolveChannel(ctx)).toBe(undefined);
});
it('returns the build profile name if "channel" is not specified in build profile and EAS Update is configured', () => {
jest.mocked(isEASUpdateConfigured).mockReturnValue(true);
const buildProfileMock = mock<BuildProfile<Platform>>();
when(buildProfileMock.channel).thenReturn(undefined);
const buildProfile = instance(buildProfileMock);
const ctxMock = mock<BuildContext<Platform>>();
when(ctxMock.exp).thenReturn({
name: 'todo',
slug: 'todo',
updates: { url: 'http://sokal.dev/todo' },
});
when(ctxMock.buildProfileName).thenReturn('production');
when(ctxMock.buildProfile).thenReturn(buildProfile);
const ctx = instance(ctxMock);
expect(resolveChannel(ctx)).toBe('production');
});
});
@@ -0,0 +1,12 @@
import { Platform } from '@expo/eas-build-job';
import { isEASUpdateConfigured } from '../../project/projectUtils';
import { BuildContext } from '../context';
export function resolveChannel(ctx: BuildContext<Platform>): string | undefined {
let fallback: string | undefined = undefined;
if (isEASUpdateConfigured(ctx.projectDir, ctx.exp)) {
fallback = ctx.buildProfileName;
}
return ctx.buildProfile.channel ?? fallback;
}
@@ -158,13 +158,9 @@ describe(getProjectIdAsync, () => {
expect(projectId).toEqual('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith(
'/app',
{
extra: { eas: { projectId: '2345' } },
},
{ skipSDKVersionRequirement: true }
);
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
extra: { eas: { projectId: '2345' } },
});
});
it('throws if writing the ID back to the config fails', async () => {
@@ -25,13 +25,9 @@ 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 } },
},
{ skipSDKVersionRequirement: true }
);
const result = await modifyConfigAsync(projectDir, {
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
});
switch (result.type) {
case 'success':
@@ -13,7 +13,6 @@ export default class Credentials extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.OptionalProjectConfig,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.Analytics,
};
@@ -22,7 +21,6 @@ export default class Credentials extends EasCommand {
const {
loggedIn: { actor, graphqlClient },
projectConfig,
getDynamicProjectConfigAsync,
analytics,
} = await this.getContextAsync(Credentials, {
nonInteractive: false,
@@ -33,7 +31,6 @@ export default class Credentials extends EasCommand {
graphqlClient,
analytics,
projectConfig ?? null,
getDynamicProjectConfigAsync,
flags.platform
).runAsync();
}
+1 -1
View File
@@ -75,7 +75,7 @@ export default class Submit extends EasCommand {
exclusive: ['latest', 'id', 'path'],
}),
verbose: Flags.boolean({
description: 'Always print logs from EAS Submit',
description: 'Always print logs from Submission Service',
default: false,
}),
wait: Flags.boolean({
+53 -94
View File
@@ -1,4 +1,4 @@
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
import { ExpoConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
@@ -28,13 +28,11 @@ import { ora } from '../../ora';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
ExpoCLIExportPlatformFlag,
PublishPlatform,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
filterExportedPlatformsByFlag,
isUploadedAssetCountAboveWarningThreshold,
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
@@ -48,13 +46,12 @@ 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: Partial<PublishPlatform>[] = ['android', 'ios'];
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
type RawUpdateFlags = {
auto: boolean;
@@ -72,7 +69,7 @@ type RawUpdateFlags = {
type UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
platform: RequestedPlatform;
branchName?: string;
updateMessage?: string;
republish: boolean;
@@ -84,21 +81,6 @@ 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';
@@ -130,11 +112,7 @@ export default class UpdatePublish extends EasCommand {
}),
platform: Flags.enum({
char: 'p',
options: [
// TODO: Add web when it's fully supported
...defaultPublishPlatforms,
'all',
],
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
@@ -199,15 +177,14 @@ export default class UpdatePublish extends EasCommand {
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(platformFlag),
platform: platformFlag,
projectDir,
projectId,
});
const { exp } = await getDynamicProjectConfigAsync();
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
let realizedPlatforms: PublishPlatform[] = [];
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
if (!branchName) {
if (autoFlag) {
@@ -285,7 +262,7 @@ export default class UpdatePublish extends EasCommand {
let publicationPlatformMessage: string;
if (platformFlag === 'all') {
if (updatesToRepublishFilteredByPlatform.length < defaultPublishPlatforms.length) {
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
@@ -302,9 +279,6 @@ export default class UpdatePublish extends EasCommand {
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
}
realizedPlatforms = updatesToRepublishFilteredByPlatform.map(
update => update.platform as PublishPlatform
);
// These are the same for each member of an update group
groupId = updatesToRepublishFilteredByPlatform[0].group;
@@ -353,44 +327,39 @@ export default class UpdatePublish extends EasCommand {
// build bundle and upload assets for a new publish
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
const bundleSpinner = ora().start('Building bundle...');
try {
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag });
bundleSpinner.succeed('Exported bundle(s)');
await buildBundlesAsync({ projectDir, inputDir, exp });
bundleSpinner.succeed('Built bundle!');
} catch (e) {
bundleSpinner.fail('Export failed');
bundleSpinner.fail('Failed to build bundle!');
throw e;
}
}
// After possibly bundling, assert that the input directory can be found.
const distRoot = await resolveInputDirectoryAsync(inputDir, { skipBundler });
const assetSpinner = ora().start('Uploading...');
const assetSpinner = ora().start('Uploading assets...');
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
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 (${totalAssets - missingAssets}/${totalAssets})`;
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 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
: `Uploading assets skipped -- no new assets found!`;
assetSpinner.succeed(uploadAssetSuccessMessage);
} catch (e) {
assetSpinner.fail('Failed to upload');
assetSpinner.fail('Failed to upload assets');
throw e;
}
}
@@ -400,16 +369,11 @@ export default class UpdatePublish extends EasCommand {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
}
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
for (const runtime of runtimeVersions) {
const platforms = runtimeVersions
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
.map(({ platform }) => platform);
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
}
const 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 { branchId } = await ensureBranchExistsAsync(graphqlClient, {
@@ -426,8 +390,8 @@ export default class UpdatePublish extends EasCommand {
const gitCommitHash = await getVcsClient().getCommitHashAsync();
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
([runtime, platforms]) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
@@ -443,7 +407,7 @@ export default class UpdatePublish extends EasCommand {
return {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion: republish ? oldRuntimeVersion : runtimeVersion,
runtimeVersion: republish ? oldRuntimeVersion : runtime,
message: truncatedMessage,
gitCommitHash,
awaitingCodeSigningInfo: !!codeSigningInfo,
@@ -514,15 +478,12 @@ export default class UpdatePublish extends EasCommand {
}
Log.addNewLineIfNone();
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
for (const runtime of new Set(Object.values(runtimeVersions))) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
update => update.runtimeVersion === runtime
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
@@ -539,7 +500,7 @@ export default class UpdatePublish extends EasCommand {
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Runtime version', value: runtime },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
@@ -601,20 +562,31 @@ export default class UpdatePublish extends EasCommand {
}
}
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
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/')}`
),
])
);
}
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platforms: PublishPlatform[],
platform: RequestedPlatform,
projectDir: string
): Promise<{ platform: string; runtimeVersion: string }[]> {
): Promise<Record<string, string>> {
const platforms = (platform === 'all' ? ['android', 'ios'] : [platform]) as Platform[];
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 as Platform)) === Workflow.MANAGED;
const isManaged = (await resolveWorkflowAsync(projectDir, 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.'
@@ -623,18 +595,5 @@ async function getRuntimeVersionObjectAsync(
}
}
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/')}`
),
};
});
return transformRuntimeVersions(exp, platforms);
}
@@ -1,5 +1,4 @@
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';
@@ -11,7 +10,6 @@ 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, CredentialsContextProjectInfo } from '../context';
import { CredentialsContext } from '../context';
import { ActionInfo, AndroidActionType, Scope } from './Actions';
import {
buildCredentialsActions,
@@ -52,16 +52,9 @@ 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,
projectInfo: this.callingAction.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, CredentialsContextProjectInfo } from '../context';
import { CredentialsContext } from '../context';
import {
AppStoreApiKeyPurpose,
selectAscApiKeysFromAccountAsync,
@@ -61,18 +61,9 @@ 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,
projectInfo: this.callingAction.projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
analytics: this.callingAction.analytics,
@@ -1,5 +1,4 @@
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';
@@ -13,7 +12,6 @@ export class SelectPlatform {
public readonly graphqlClient: ExpoGraphqlClient,
public readonly analytics: Analytics,
public readonly projectInfo: CredentialsContextProjectInfo | null,
public readonly getDynamicProjectConfigAsync: DynamicConfigContextFn,
private readonly flagPlatform?: string
) {}
+7 -20
View File
@@ -2523,12 +2523,6 @@ export type GetSignedAssetUploadSpecificationsResult = {
specifications: Array<Scalars['String']>;
};
export enum GitHubAppEnvironment {
Development = 'DEVELOPMENT',
Production = 'PRODUCTION',
Staging = 'STAGING'
}
export type GitHubAppInstallation = {
__typename?: 'GitHubAppInstallation';
accessibleRepositories: GitHubRepositoryPaginationResult;
@@ -2592,9 +2586,7 @@ export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
environment: GitHubAppEnvironment;
installation: GitHubAppInstallation;
name: Scalars['String'];
searchRepositories: GitHubRepositoryPaginationResult;
};
@@ -2621,14 +2613,12 @@ 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'];
};
@@ -3297,7 +3287,6 @@ export type ProjectArchiveSourceInput = {
};
export enum ProjectArchiveSourceType {
Gcs = 'GCS',
S3 = 'S3',
Url = 'URL'
}
@@ -5017,7 +5006,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, 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 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 CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
@@ -5027,7 +5016,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, 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 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 CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -5083,7 +5072,6 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: AndroidSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5093,7 +5081,6 @@ export type CreateIosSubmissionMutationVariables = Exact<{
appId: Scalars['ID'];
config: IosSubmissionConfigInput;
submittedBuildId?: InputMaybe<Scalars['ID']>;
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
}>;
@@ -5184,14 +5171,14 @@ export type BuildsByIdQueryVariables = Exact<{
}>;
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 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 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, 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 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 ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5201,7 +5188,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, 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 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 ViewUpdateChannelOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5318,9 +5305,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, 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 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 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 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 EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -28,16 +28,10 @@ export const SubmissionMutation = {
$appId: ID!
$config: AndroidSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createAndroidSubmission(
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
) {
submission {
id
@@ -66,16 +60,10 @@ export const SubmissionMutation = {
$appId: ID!
$config: IosSubmissionConfigInput!
$submittedBuildId: ID
$archiveSource: SubmissionArchiveSourceInput
) {
submission {
createIosSubmission(
input: {
appId: $appId
config: $config
submittedBuildId: $submittedBuildId
archiveSource: $archiveSource
}
input: { appId: $appId, config: $config, submittedBuildId: $submittedBuildId }
) {
submission {
id
@@ -8,17 +8,16 @@ import {
UploadSessionType,
} from '../generated';
export interface SignedUrl {
export interface PresignedPost {
url: string;
headers: Record<string, string>;
bucketKey: string;
fields: Record<string, string>;
}
export const UploadSessionMutation = {
async createUploadSessionAsync(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType
): Promise<SignedUrl> {
): Promise<PresignedPost> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUploadSessionMutation, CreateUploadSessionMutationVariables>(
@@ -45,7 +45,6 @@ export const BuildFragmentNode = gql`
appBuildVersion
runtimeVersion
gitCommitHash
gitCommitMessage
initialQueuePosition
queuePosition
estimatedWaitTimeLeftSeconds
@@ -1,67 +0,0 @@
import { ProjectConfig, getConfig } from '@expo/config';
import { getExpoConfig } from '../expoConfig';
jest.mock('@expo/config');
describe(getExpoConfig, () => {
it('correctly restores process.env as best as possible', () => {
process.env.BEFORE_CALL_KEY = 'BEFORE';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN = 'BEFORE_OVERRIDDEN_BY_WITHIN';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS = 'BEFORE_OVERRIDDEN_BY_OPTS';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN =
'BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN';
jest.mocked(getConfig).mockImplementation(() => {
process.env.WITHIN_CALL_KEY = 'WITHIN';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN = 'OVERRIDDEN_VALUE';
process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN = 'OVERRIDDEN_VALUE';
process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE =
'OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE';
process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN = 'OVERRIDDEN_WITHIN';
return {} as ProjectConfig;
});
// ensure idempotent (run N times)
for (let i = 0; i < 2; i++) {
getExpoConfig('', {
env: {
OPTS_KEY: 'OPTS',
BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS: 'OVERRIDDEN_VALUE',
OPTS_KEY_OVERRIDDEN_BY_WITHIN: 'OPTS_KEY_OVERRIDDEN_BY_WITHIN',
OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE:
'OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE',
BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN: 'OVERRIDDEN_OPTS',
},
});
// env from before the call should be restored
expect(process.env.BEFORE_CALL_KEY).toEqual('BEFORE');
// env from before the call but overridden within the call should not be restored
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_WITHIN).toEqual('OVERRIDDEN_VALUE');
// env from before the call but overridden by opts should be restored
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS).toEqual('BEFORE_OVERRIDDEN_BY_OPTS');
// env added within the call should be kept
expect(process.env.WITHIN_CALL_KEY).toEqual('WITHIN');
// env added by opts should be removed
expect(process.env.OPTS_KEY).toBeUndefined();
// env added by opts but overridden within the call should be retained and have overridden value
expect(process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN).toEqual('OVERRIDDEN_VALUE');
// env added by opts but overridden within the call to the same value will be removed
// note that this is a known bug/limitation of this approach
expect(process.env.OPTS_KEY_OVERRIDDEN_BY_WITHIN_TO_SAME_VALUE).toBeUndefined();
// env from before but overridden within the call and by opts should retain value from
// within the call
expect(process.env.BEFORE_CALL_KEY_OVERRIDDEN_BY_OPTS_AND_WITHIN).toEqual(
'OVERRIDDEN_WITHIN'
);
}
});
});
@@ -0,0 +1,76 @@
import { getPackageJson } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import * as projectUtils from '../projectUtils';
jest.mock('@expo/config', () => ({
getPackageJson: jest.fn(),
}));
describe(projectUtils.isEASUpdateConfigured, () => {
describe('expo-updates is not installed', () => {
it('returns false if updates.url is not set', () => {
jest.mocked(getPackageJson).mockReturnValue({});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(false);
});
it('returns false if updates.url is set', () => {
jest.mocked(getPackageJson).mockReturnValue({});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
updates: { url: 'http://sokal.dev/todo' },
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(false);
});
it('returns true if updates.url is set (for SDK < 44)', () => {
jest.mocked(getPackageJson).mockReturnValue({});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
sdkVersion: '43.0.0',
updates: { url: 'http://sokal.dev/todo' },
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(true);
});
});
describe('expo-updates is installed', () => {
it('returns false if updates.url is not set', () => {
jest.mocked(getPackageJson).mockReturnValue({
dependencies: {
'expo-updates': '1.2.3',
},
});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(false);
});
it('returns true if updates.url is set', () => {
jest.mocked(getPackageJson).mockReturnValue({
dependencies: {
'expo-updates': '1.2.3',
},
});
const exp: ExpoConfig = {
name: 'blah',
slug: 'blah',
updates: { url: 'http://sokal.dev/todo' },
};
expect(projectUtils.isEASUpdateConfigured('/path/to/project', exp)).toBe(true);
});
});
});
@@ -14,7 +14,6 @@ import {
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
convertAssetToUpdateInfoGroupFormatAsync,
filterExportedPlatformsByFlag,
filterOutAssetsThatAlreadyExistAsync,
getBase64URLEncoding,
getStorageKey,
@@ -92,28 +91,6 @@ 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');
@@ -259,41 +236,30 @@ describe(resolveInputDirectoryAsync, () => {
it('returns the correct distRoot path', async () => {
const customDirectoryName = path.resolve(uuidv4());
await fs.mkdir(customDirectoryName, { recursive: true });
expect(await resolveInputDirectoryAsync(customDirectoryName, { skipBundler: false })).toBe(
customDirectoryName
);
expect(await resolveInputDirectoryAsync(customDirectoryName)).toBe(customDirectoryName);
});
it('throws an error if the path does not exist', async () => {
const nonExistentPath = path.resolve(uuidv4());
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`
);
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.`);
});
});
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',
web: 'web-bundle-code',
};
const inputDir = path.resolve(uuidv4());
const bundles = { android: 'android-bundle-code', ios: 'ios-bundle-code' };
const inputDir = uuidv4();
const userDefinedAssets = [
{
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: `${inputDir}/assets/${fakeHash}`,
path: path.resolve(`${inputDir}/assets/${fakeHash}`),
},
];
@@ -319,20 +285,16 @@ 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)).toEqual({
expect(await collectAssetsAsync({ inputDir, platforms: defaultPublishPlatforms })).toEqual({
android: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/android.js`,
path: path.resolve(`${inputDir}/bundles/android.js`),
},
assets: userDefinedAssets,
},
@@ -340,15 +302,18 @@ describe(collectAssetsAsync, () => {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/ios.js`,
path: path.resolve(`${inputDir}/bundles/ios.js`),
},
assets: userDefinedAssets,
},
web: {
});
expect(await collectAssetsAsync({ inputDir, platforms: ['ios'] })).toEqual({
ios: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/web.js`,
path: path.resolve(`${inputDir}/bundles/ios.js`),
},
assets: userDefinedAssets,
},
+1 -25
View File
@@ -1,6 +1,5 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
export interface ExpoConfigOptions {
env?: Env;
@@ -10,11 +9,6 @@ export interface ExpoConfigOptions {
export function getExpoConfig(projectDir: string, opts: ExpoConfigOptions = {}): ExpoConfig {
const originalProcessEnv: NodeJS.ProcessEnv = process.env;
const optsEnvKeys = opts.env ? Object.keys(opts.env) : [];
const originalEnvKeys = new Set(Object.keys(process.env));
const newKeysAddedToProcessEnv = new Set(optsEnvKeys.filter(k => !originalEnvKeys.has(k)));
try {
process.env = {
...process.env,
@@ -26,24 +20,6 @@ export function getExpoConfig(projectDir: string, opts: ExpoConfigOptions = {}):
});
return exp;
} finally {
optsEnvKeys.forEach(k => {
// if new key added, remove unless it was modified
if (newKeysAddedToProcessEnv.has(k)) {
if (k in originalProcessEnv) {
if (process.env[k] === originalProcessEnv[k]) {
delete process.env[k];
}
} else {
if (nullthrows(opts.env)[k] === process.env[k]) {
delete process.env[k];
}
}
} else {
// opts key overrode env key, but only if the value is equal to the one declared by opts
if (process.env[k] === nullthrows(opts.env)[k]) {
process.env[k] = originalProcessEnv[k];
}
}
});
process.env = originalProcessEnv;
}
}
+6 -5
View File
@@ -64,18 +64,19 @@ export function isExpoUpdatesInstalled(projectDir: string): boolean {
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
}
export function isExpoUpdatesInstalledOrAvailable(
projectDir: string,
sdkVersion?: string
): boolean {
export function isExpoUpdatesInstalledOrAvailable(projectDir: string, exp: ExpoConfig): boolean {
// before sdk 44, expo-updates was included in with the expo module
if (sdkVersion && semver.lt(sdkVersion, '44.0.0')) {
if (exp.sdkVersion && semver.lt(exp.sdkVersion, '44.0.0')) {
return true;
}
return isExpoUpdatesInstalled(projectDir);
}
export function isEASUpdateConfigured(projectDir: string, exp: ExpoConfig): boolean {
return isExpoUpdatesInstalledOrAvailable(projectDir, exp) && exp.updates?.url !== undefined;
}
export async function validateAppVersionRuntimePolicySupportAsync(
projectDir: string,
exp: ExpoConfig
+42 -77
View File
@@ -10,20 +10,19 @@ 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 Log from '../log';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import uniqBy from '../utils/expodash/uniqBy';
export type ExpoCLIExportPlatformFlag = Platform | 'all';
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
type Metadata = {
version: number;
bundler: 'metro';
fileMetadata: {
[key in Platform]: { assets: { path: string; ext: string }[]; bundle: string };
[key in 'android' | 'ios']: { assets: { path: string; ext: string }[]; bundle: string };
};
};
export type RawAsset = {
@@ -32,7 +31,7 @@ export type RawAsset = {
path: string;
};
type CollectedAssets = {
[platform in Platform]?: {
[platform in PublishPlatform]?: {
launchAsset: RawAsset;
assets: RawAsset[];
};
@@ -48,7 +47,7 @@ type ManifestFragment = {
extra?: ManifestExtra;
};
type UpdateInfoGroup = {
[key in Platform]: ManifestFragment;
[key in PublishPlatform]: ManifestFragment;
};
const fileMetadataJoi = Joi.object({
@@ -56,14 +55,13 @@ const fileMetadataJoi = Joi.object({
.required()
.items(Joi.object({ path: Joi.string().required(), ext: Joi.string().required() })),
bundle: Joi.string().required(),
}).optional();
}).required();
export const MetadataJoi = Joi.object({
version: Joi.number().required(),
bundler: Joi.string().required(),
fileMetadata: Joi.object({
android: fileMetadataJoi,
ios: fileMetadataJoi,
web: fileMetadataJoi,
}).required(),
}).required();
@@ -131,7 +129,7 @@ export async function buildUnsortedUpdateInfoGroupAsync(
assets: CollectedAssets,
exp: ExpoConfig
): Promise<UpdateInfoGroup> {
let platform: Platform;
let platform: PublishPlatform;
const updateInfoGroup: Partial<UpdateInfoGroup> = {};
for (platform in assets) {
updateInfoGroup[platform] = {
@@ -151,12 +149,10 @@ 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) {
@@ -164,14 +160,7 @@ export async function buildBundlesAsync({
}
if (shouldUseVersionedExpoCLI(projectDir, exp)) {
await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--dump-sourcemap',
'--platform',
platformFlag,
]);
await expoCommandAsync(projectDir, ['export', '--output-dir', inputDir, '--dump-sourcemap']);
} else {
// Legacy global Expo CLI
await expoCommandAsync(projectDir, [
@@ -181,23 +170,18 @@ export async function buildBundlesAsync({
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
'--platform',
platformFlag,
]);
}
}
export async function resolveInputDirectoryAsync(
inputDir: string,
{ skipBundler }: { skipBundler?: boolean }
): Promise<string> {
const distRoot = path.resolve(inputDir);
export async function resolveInputDirectoryAsync(customInputDirectory: string): Promise<string> {
const distRoot = path.resolve(customInputDirectory);
if (!(await fs.pathExists(distRoot))) {
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);
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.`);
}
return distRoot;
}
@@ -216,62 +200,43 @@ 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 function filterExportedPlatformsByFlag<T extends Partial<Record<Platform, any>>>(
record: T,
platformFlag: ExpoCLIExportPlatformFlag
): T {
if (platformFlag === 'all') {
return record;
}
export async function collectAssetsAsync({
inputDir,
platforms,
}: {
inputDir: string;
platforms: PublishPlatform[];
}): Promise<CollectedAssets> {
const distRoot = await resolveInputDirectoryAsync(inputDir);
const metadata = loadMetadata(distRoot);
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] = {
const assetsFinal: CollectedAssets = {};
for (const platform of platforms) {
assetsFinal[platform] = {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(dir, metadata.fileMetadata[platform].bundle),
path: path.resolve(distRoot, metadata.fileMetadata[platform].bundle),
},
assets: metadata.fileMetadata[platform].assets.map(asset => ({
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
contentType: guessContentTypeFromExtension(asset.ext),
path: path.join(dir, asset.path),
})),
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),
};
}),
};
}
return collectedAssets;
}
// ensure the file extension has a '.' prefix
function ensureLeadingPeriod(extension: string): string {
return extension.startsWith('.') ? extension : `.${extension}`;
return assetsFinal;
}
export async function filterOutAssetsThatAlreadyExistAsync(
+1 -4
View File
@@ -14,10 +14,7 @@ export async function promptAsync<T extends string = string>(
options: Options = {}
): Promise<Answers<T>> {
if (!process.stdin.isTTY && !global.test) {
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}`
);
throw new Error('Input is required, but stdin is not readable.');
}
return await prompts<T>(questions, {
onCancel() {
+107 -105
View File
@@ -5,13 +5,12 @@ import { URL } from 'url';
import * as uuid from 'uuid';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { BuildFragment } from '../graphql/generated';
import { AppPlatform, 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';
@@ -22,21 +21,15 @@ 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 {
@@ -58,11 +51,6 @@ interface ArchiveBuildIdSource extends ArchiveSourceBase {
id: string;
}
interface ArchiveBuildSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.build;
build: BuildFragment;
}
interface ArchiveBuildListSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.buildList;
}
@@ -71,10 +59,10 @@ interface ArchivePromptSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.prompt;
}
interface ArchiveGCSSource extends ArchiveSourceBase {
sourceType: ArchiveSourceType.gcs;
bucketKey: string;
localSource: ArchivePathSource;
export interface Archive {
build?: BuildFragment;
source: ArchiveSource;
url?: string;
}
export type ArchiveSource =
@@ -82,62 +70,54 @@ export type ArchiveSource =
| ArchiveLatestSource
| ArchivePathSource
| ArchiveBuildIdSource
| ArchiveBuildSource
| ArchiveBuildListSource
| ArchivePromptSource
| ArchiveGCSSource;
export type ResolvedArchiveSource = ArchiveUrlSource | ArchiveGCSSource | ArchiveBuildSource;
| ArchivePromptSource;
export async function getArchiveAsync(
ctx: ArchiveResolverContext,
graphqlClient: ExpoGraphqlClient,
source: ArchiveSource
): Promise<ResolvedArchiveSource> {
): Promise<Archive> {
switch (source.sourceType) {
case ArchiveSourceType.prompt: {
return await handlePromptSourceAsync(ctx);
return await handlePromptSourceAsync(graphqlClient, source);
}
case ArchiveSourceType.url: {
return await handleUrlSourceAsync(ctx, source);
return await handleUrlSourceAsync(graphqlClient, source);
}
case ArchiveSourceType.latest: {
return await handleLatestSourceAsync(ctx);
return await handleLatestSourceAsync(graphqlClient, source);
}
case ArchiveSourceType.path: {
return await handlePathSourceAsync(ctx, source);
return await handlePathSourceAsync(graphqlClient, source);
}
case ArchiveSourceType.buildId: {
return await handleBuildIdSourceAsync(ctx, source);
return await handleBuildIdSourceAsync(graphqlClient, source);
}
case ArchiveSourceType.buildList: {
return await handleBuildListSourceAsync(ctx);
}
case ArchiveSourceType.gcs: {
return source;
}
case ArchiveSourceType.build: {
return source;
return await handleBuildListSourceAsync(graphqlClient, source);
}
}
}
async function handleUrlSourceAsync(
ctx: ArchiveResolverContext,
graphqlClient: ExpoGraphqlClient,
source: ArchiveUrlSource
): Promise<ResolvedArchiveSource> {
): Promise<Archive> {
const { url } = source;
if (!validateUrl(url)) {
Log.error(chalk.bold(`The URL you provided is invalid: ${url}`));
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
const maybeBuildId = isBuildDetailsPage(url);
if (maybeBuildId) {
if (await askIfUseBuildIdFromUrlAsync(ctx, source, maybeBuildId)) {
return getArchiveAsync(ctx, {
if (await askIfUseBuildIdFromUrlAsync(source, maybeBuildId)) {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.buildId,
id: maybeBuildId,
});
@@ -145,19 +125,20 @@ async function handleUrlSourceAsync(
}
return {
sourceType: ArchiveSourceType.url,
url,
source,
};
}
async function handleLatestSourceAsync(
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
graphqlClient: ExpoGraphqlClient,
source: ArchiveLatestSource
): Promise<Archive> {
try {
const [latestBuild] = await getRecentBuildsForSubmissionAsync(
ctx.graphqlClient,
toAppPlatform(ctx.platform),
ctx.projectId
graphqlClient,
toAppPlatform(source.platform),
source.projectId
);
if (!latestBuild) {
@@ -166,14 +147,15 @@ 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(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build: latestBuild,
source,
};
} catch (err) {
Log.error(err);
@@ -182,37 +164,34 @@ async function handleLatestSourceAsync(
}
async function handlePathSourceAsync(
ctx: ArchiveResolverContext,
graphqlClient: ExpoGraphqlClient,
source: ArchivePathSource
): Promise<ResolvedArchiveSource> {
): Promise<Archive> {
if (!(await isExistingFileAsync(source.path))) {
Log.error(chalk.bold(`${source.path} doesn't exist`));
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
Log.log('Uploading your app archive to EAS Submit');
const bucketKey = await uploadAppArchiveAsync(ctx.graphqlClient, source.path);
Log.log('Uploading your app archive to the Expo Submission Service');
const uploadUrl = await uploadAppArchiveAsync(graphqlClient, source.path);
return {
sourceType: ArchiveSourceType.gcs,
bucketKey,
localSource: {
sourceType: ArchiveSourceType.path,
path: source.path,
},
url: uploadUrl,
source,
};
}
async function handleBuildIdSourceAsync(
ctx: ArchiveResolverContext,
graphqlClient: ExpoGraphqlClient,
source: ArchiveBuildIdSource
): Promise<ResolvedArchiveSource> {
): Promise<Archive> {
try {
const build = await BuildQuery.byIdAsync(ctx.graphqlClient, source.id);
const build = await BuildQuery.byIdAsync(graphqlClient, source.id);
if (build.platform !== toAppPlatform(ctx.platform)) {
const expectedPlatformName = appPlatformDisplayNames[toAppPlatform(ctx.platform)];
if (build.platform !== toAppPlatform(source.platform)) {
const expectedPlatformName = appPlatformDisplayNames[toAppPlatform(source.platform)];
const receivedPlatformName = appPlatformDisplayNames[build.platform];
Log.error(
chalk.bold(
@@ -220,14 +199,15 @@ async function handleBuildIdSourceAsync(
)
);
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build,
source,
};
} catch (err) {
Log.error(chalk.bold(`Could not find build with ID ${source.id}`));
@@ -239,24 +219,26 @@ async function handleBuildIdSourceAsync(
);
Log.debug('Original error:', err);
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
}
async function handleBuildListSourceAsync(
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
graphqlClient: ExpoGraphqlClient,
source: ArchiveBuildListSource
): Promise<Archive> {
try {
const appPlatform = toAppPlatform(ctx.platform);
const appPlatform = toAppPlatform(source.platform);
const expiryDate = new Date(); // artifacts expire after 30 days
expiryDate.setDate(expiryDate.getDate() - 30);
const recentBuilds = await getRecentBuildsForSubmissionAsync(
ctx.graphqlClient,
graphqlClient,
appPlatform,
ctx.projectId,
source.projectId,
{
limit: BUILD_LIST_ITEM_COUNT,
}
@@ -269,7 +251,8 @@ async function handleBuildListSourceAsync(
"It looks like you haven't run 'eas build' yet."
)
);
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
@@ -281,7 +264,8 @@ async function handleBuildListSourceAsync(
'EAS keeps your build artifacts only for 30 days.'
)
);
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
@@ -301,14 +285,15 @@ async function handleBuildListSourceAsync(
});
if (selectedBuild == null) {
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build: selectedBuild,
source,
};
} catch (err) {
Log.error(err);
@@ -317,39 +302,53 @@ async function handleBuildListSourceAsync(
}
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
const { id, updatedAt, runtimeVersion, buildProfile, gitCommitHash, gitCommitMessage, channel } =
build;
const {
id,
platform,
updatedAt,
appVersion,
sdkVersion,
runtimeVersion,
buildProfile,
appBuildVersion,
releaseChannel,
initiatingActor,
} = 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 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}`,
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)}`,
].join('\n');
return {
title,
description,
value: build,
disabled: buildDate < expiryDate,
};
}
async function handlePromptSourceAsync(
ctx: ArchiveResolverContext
): Promise<ResolvedArchiveSource> {
graphqlClient: ExpoGraphqlClient,
source: ArchivePromptSource
): Promise<Archive> {
const { sourceType: sourceTypeRaw } = await promptAsync({
name: 'sourceType',
type: 'select',
@@ -373,27 +372,31 @@ async function handlePromptSourceAsync(
const sourceType = sourceTypeRaw as ArchiveSourceType;
switch (sourceType) {
case ArchiveSourceType.url: {
const url = await askForArchiveUrlAsync(ctx.platform);
return getArchiveAsync(ctx, {
const url = await askForArchiveUrlAsync(source.platform);
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.url,
url,
});
}
case ArchiveSourceType.path: {
const path = await askForArchivePathAsync(ctx.platform);
return getArchiveAsync(ctx, {
const path = await askForArchivePathAsync(source.platform);
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.path,
path,
});
}
case ArchiveSourceType.buildList: {
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.buildList,
});
}
case ArchiveSourceType.buildId: {
const id = await askForBuildIdAsync();
return getArchiveAsync(ctx, {
return getArchiveAsync(graphqlClient, {
...source,
sourceType: ArchiveSourceType.buildId,
id,
});
@@ -463,14 +466,13 @@ 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 (!ctx.nonInteractive) {
if (!source.nonInteractive) {
const useAsBuildId = await confirmAsync({
message: `Do you want to submit build ${buildId} instead?`,
});
@@ -5,15 +5,12 @@ 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> {
@@ -22,7 +19,6 @@ export interface SubmissionInput<P extends Platform> {
? AndroidSubmissionConfigInput
: IosSubmissionConfigInput;
buildId?: string;
archiveSource?: SubmissionArchiveSourceInput;
}
interface AnalyticEvents {
@@ -75,27 +71,6 @@ 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,
SubmissionArchiveSourceType,
UploadSessionType,
} from '../../graphql/generated';
import { AppPlatform, BuildFragment, UploadSessionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import { confirmAsync, promptAsync } from '../../prompts';
import { uploadFileAtPathToGCSAsync } from '../../uploads';
import { ArchiveSourceType, BUILD_LIST_ITEM_COUNT, getArchiveAsync } from '../ArchiveSource';
import { uploadFileAtPathToS3Async } from '../../uploads';
import {
Archive,
ArchiveSourceType,
BUILD_LIST_ITEM_COUNT,
getArchiveAsync,
} from '../ArchiveSource';
import { getRecentBuildsForSubmissionAsync } from '../utils/builds';
jest.mock('fs');
@@ -28,15 +28,12 @@ jest.mock('../../graphql/queries/BuildQuery', () => ({
},
}));
const ARCHIVE_SOURCE = {
type: SubmissionArchiveSourceType.Url,
url: 'https://url.to/archive.tar.gz',
};
const ARCHIVE_URL = 'https://url.to/archive.tar.gz';
const MOCK_BUILD_FRAGMENT: Partial<BuildFragment> = {
id: uuidv4(),
artifacts: {
buildUrl: ARCHIVE_SOURCE.url,
buildUrl: ARCHIVE_URL,
},
appVersion: '1.2.3',
platform: AppPlatform.Android,
@@ -70,82 +67,72 @@ describe(getArchiveAsync, () => {
});
it('handles URL source', async () => {
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.url,
url: ARCHIVE_SOURCE.url,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.url,
url: ARCHIVE_URL,
});
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('prompts again if provided URL is invalid', async () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.url,
url: 'invalid',
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.url,
url: 'invalid',
});
expect(promptAsync).toHaveBeenCalledTimes(2);
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, 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(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.url,
url: 'https://expo.dev/accounts/turtle/projects/blah/builds/81da6b36-efe4-4262-8970-84f03efeec81',
}
);
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',
});
expect(confirmAsync).toHaveBeenCalled();
expect(archive.sourceType).toBe(ArchiveSourceType.build);
expect((archive as any).build.id).toBe(MOCK_BUILD_FRAGMENT.id);
assertArchiveResult(archive, ArchiveSourceType.buildId);
expect((archive.source as any).id).toBe('81da6b36-efe4-4262-8970-84f03efeec81');
});
it('handles prompt source', async () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.prompt,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.prompt,
});
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, 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(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildId,
id: buildId,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildId,
id: buildId,
});
expect(BuildQuery.byIdAsync).toBeCalledWith(graphqlClient, buildId);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
assertArchiveResult(archive, ArchiveSourceType.buildId);
});
it('prompts again if build with provided ID doesnt exist', async () => {
@@ -153,17 +140,15 @@ describe(getArchiveAsync, () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildId,
id: uuidv4(),
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildId,
id: uuidv4(),
});
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles latest build source', async () => {
@@ -172,19 +157,18 @@ describe(getArchiveAsync, () => {
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.latest,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
projectId,
sourceType: ArchiveSourceType.latest,
});
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
toAppPlatform(SOURCE_STUB_INPUT.platform),
projectId
);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
assertArchiveResult(archive, ArchiveSourceType.latest);
});
it('prompts again if no builds exists when selected latest', async () => {
@@ -192,16 +176,14 @@ describe(getArchiveAsync, () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.latest,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.latest,
});
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles build-list-select source', async () => {
@@ -211,12 +193,11 @@ describe(getArchiveAsync, () => {
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
jest.mocked(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient, projectId },
{
sourceType: ArchiveSourceType.buildList,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
projectId,
sourceType: ArchiveSourceType.buildList,
});
expect(getRecentBuildsForSubmissionAsync).toBeCalledWith(
graphqlClient,
@@ -224,7 +205,7 @@ describe(getArchiveAsync, () => {
projectId,
{ limit: BUILD_LIST_ITEM_COUNT }
);
expect(archive.sourceType).toBe(ArchiveSourceType.build);
assertArchiveResult(archive, ArchiveSourceType.buildList);
});
it('prompts again if all builds have expired', async () => {
@@ -237,16 +218,14 @@ describe(getArchiveAsync, () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildList,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('falls back to prompt if user selected "None of the above"', async () => {
@@ -257,62 +236,62 @@ describe(getArchiveAsync, () => {
.mocked(promptAsync)
.mockResolvedValueOnce({ selectedBuild: null })
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.buildList,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.buildList,
});
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, ArchiveSourceType.url);
});
it('handles path source', async () => {
const path = '/archive.apk';
await fs.writeFile(path, 'some content');
jest.mocked(uploadFileAtPathToGCSAsync).mockResolvedValueOnce('wat');
jest
.mocked(uploadFileAtPathToS3Async)
.mockResolvedValueOnce({ url: ARCHIVE_URL, bucketKey: 'wat' });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.path,
path,
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.path,
path,
});
expect(uploadFileAtPathToGCSAsync).toBeCalledWith(
expect(uploadFileAtPathToS3Async).toBeCalledWith(
graphqlClient,
UploadSessionType.EasSubmitGcsAppArchive,
UploadSessionType.EasSubmitAppArchive,
path,
expect.anything()
);
expect(archive).toMatchObject({
sourceType: ArchiveSourceType.gcs,
bucketKey: 'wat',
localSource: {
sourceType: ArchiveSourceType.path,
path,
},
});
assertArchiveResult(archive, ArchiveSourceType.path);
});
it('prompts again if provided path doesnt exist', async () => {
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_SOURCE.url });
.mockResolvedValueOnce({ url: ARCHIVE_URL });
const archive = await getArchiveAsync(
{ ...SOURCE_STUB_INPUT, graphqlClient },
{
sourceType: ArchiveSourceType.path,
path: './doesnt/exist.aab',
}
);
const archive = await getArchiveAsync(graphqlClient, {
...SOURCE_STUB_INPUT,
sourceType: ArchiveSourceType.path,
path: './doesnt/exist.aab',
});
expect(archive.sourceType).toBe(ArchiveSourceType.url);
assertArchiveResult(archive, 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));
return result(resolveArchiveSource(this.ctx, Platform.ANDROID));
} 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 { ArchiveSource, ResolvedArchiveSource, getArchiveAsync } from '../ArchiveSource';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import { SubmissionContext } from '../context';
import {
@@ -35,7 +35,7 @@ export interface AndroidSubmissionOptions
}
interface ResolvedSourceOptions {
archive: ResolvedArchiveSource;
archive: Archive;
serviceAccountKeyResult: ServiceAccountKeyResult;
}
@@ -47,16 +47,7 @@ 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(
{
graphqlClient: ctx.graphqlClient,
platform: Platform.ANDROID,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
},
this.options.archiveSource
),
archive: async () => await getArchiveAsync(ctx.graphqlClient, this.options.archiveSource),
// eslint-disable-next-line async-protect/async-suffix
serviceAccountKeyResult: async () => {
return await getServiceAccountKeyResultAsync(this.ctx, this.options.serviceAccountSource);
@@ -80,7 +71,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
public async createSubmissionInputAsync(
resolvedSourceOptions: ResolvedSourceOptions
): Promise<SubmissionInput<Platform.ANDROID>> {
const submissionConfig = this.formatSubmissionConfig(this.options, resolvedSourceOptions);
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
printSummary(
this.prepareSummaryData(this.options, resolvedSourceOptions),
@@ -89,7 +80,7 @@ export default class AndroidSubmitter extends BaseSubmitter<
return {
projectId: this.options.projectId,
submissionConfig,
...this.formatArchive(resolvedSourceOptions.archive),
buildId: resolvedSourceOptions.archive.build?.id,
};
}
@@ -97,22 +88,21 @@ 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,
{ serviceAccountKeyResult }: ResolvedSourceOptions
{ archive, serviceAccountKeyResult }: ResolvedSourceOptions
): AndroidSubmissionConfigInput {
const { track, releaseStatus, changesNotSentForReview } = options;
return {
archiveUrl: archive.url,
track,
changesNotSentForReview,
releaseStatus,
@@ -15,10 +15,9 @@ import {
BuildFragment,
SubmissionAndroidReleaseStatus,
SubmissionAndroidTrack,
SubmissionArchiveSourceType,
} from '../../../graphql/generated';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context';
import { getRecentBuildsForSubmissionAsync } from '../../utils/builds';
@@ -105,7 +104,7 @@ describe(AndroidSubmitCommand, () => {
});
describe('sending submission', () => {
it('sends a request to EAS Submit', async () => {
it('sends a request to Submission Service', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
@@ -135,8 +134,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,
@@ -12,7 +12,7 @@ import {
testProjectId,
} from '../../../credentials/__tests__/fixtures-constants';
import { SetUpGoogleServiceAccountKey } from '../../../credentials/android/actions/SetUpGoogleServiceAccountKey';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
+19 -1
View File
@@ -3,7 +3,10 @@ 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>): ArchiveSource {
export function resolveArchiveSource<T extends Platform>(
ctx: SubmissionContext<T>,
platform: T
): ArchiveSource {
const { url, path, id, latest } = ctx.archiveFlags;
const chosenOptions = [url, path, id, latest];
if (chosenOptions.filter(opt => opt).length > 1) {
@@ -14,11 +17,17 @@ export function resolveArchiveSource<T extends Platform>(ctx: SubmissionContext<
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)) {
@@ -27,16 +36,25 @@ export function resolveArchiveSource<T extends Platform>(ctx: SubmissionContext<
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));
return result(resolveArchiveSource(this.ctx, Platform.IOS));
} 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 { ArchiveSource, ResolvedArchiveSource, getArchiveAsync } from '../ArchiveSource';
import { Archive, ArchiveSource, getArchiveAsync } from '../ArchiveSource';
import BaseSubmitter, { SubmissionInput } from '../BaseSubmitter';
import { SubmissionContext } from '../context';
import {
@@ -40,7 +40,7 @@ export interface IosSubmissionOptions
}
interface ResolvedSourceOptions {
archive: ResolvedArchiveSource;
archive: Archive;
credentials: {
appSpecificPassword?: AppSpecificPasswordCredentials;
ascApiKeyResult?: AscApiKeyResult;
@@ -55,16 +55,7 @@ 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(
{
graphqlClient: ctx.graphqlClient,
platform: Platform.IOS,
projectId: ctx.projectId,
nonInteractive: ctx.nonInteractive,
},
this.options.archiveSource
),
archive: async () => await getArchiveAsync(ctx.graphqlClient, this.options.archiveSource),
// eslint-disable-next-line async-protect/async-suffix
credentials: async () => {
const maybeAppSpecificPassword = this.options.appSpecificPasswordSource
@@ -114,7 +105,7 @@ export default class IosSubmitter extends BaseSubmitter<
return {
projectId: this.options.projectId,
submissionConfig,
...this.formatArchive(resolvedSourceOptions.archive),
buildId: resolvedSourceOptions.archive.build?.id,
};
}
@@ -122,25 +113,24 @@ 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,
{ credentials }: ResolvedSourceOptions
{ archive, 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),
};
@@ -12,7 +12,7 @@ import {
} from '../../../credentials/__tests__/fixtures-constants';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { promptAsync } from '../../../prompts';
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
import {
@@ -12,7 +12,7 @@ import {
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpSubmissionCredentials } from '../../../credentials/ios/actions/SetUpSubmissionCredentials';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { createTestProject } from '../../../project/__tests__/utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
@@ -9,9 +9,8 @@ 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 { createTestProject } from '../../../project/__tests__/utils';
import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context';
import IosSubmitCommand from '../IosSubmitCommand';
@@ -81,7 +80,7 @@ describe(IosSubmitCommand, () => {
});
describe('sending submission', () => {
it('sends a request to EAS Submit', async () => {
it('sends a request to Submission Service', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
@@ -111,8 +110,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',
+4 -4
View File
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { UploadSessionType } from '../../graphql/generated';
import { uploadFileAtPathToGCSAsync } from '../../uploads';
import { uploadFileAtPathToS3Async } 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 bucketKey = await uploadFileAtPathToGCSAsync(
const { url } = await uploadFileAtPathToS3Async(
graphqlClient,
UploadSessionType.EasSubmitGcsAppArchive,
UploadSessionType.EasSubmitAppArchive,
path,
createProgressTracker({
total: fileSize,
@@ -29,5 +29,5 @@ export async function uploadAppArchiveAsync(
completedMessage: 'Uploaded to EAS Submit',
})
);
return bucketKey;
return url;
}
+9 -10
View File
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { AppPlatform, BuildFragment } from '../../graphql/generated';
import Log from '../../log';
import formatFields from '../../utils/formatFields';
import { ArchiveSourceType, ResolvedArchiveSource } from '../ArchiveSource';
import { Archive, ArchiveSourceType } from '../ArchiveSource';
export interface ArchiveSourceSummaryFields {
archiveUrl?: string;
archivePath?: string;
@@ -43,20 +43,19 @@ function formatSubmissionBuildSummary(build: BuildFragment): string {
);
}
export function formatArchiveSourceSummary(
archive: ResolvedArchiveSource
): ArchiveSourceSummaryFields {
export function formatArchiveSourceSummary({ source, build }: Archive): ArchiveSourceSummaryFields {
const summarySlice: ArchiveSourceSummaryFields = {};
switch (archive.sourceType) {
case ArchiveSourceType.gcs:
summarySlice.archivePath = archive.localSource.path;
switch (source.sourceType) {
case ArchiveSourceType.path:
summarySlice.archivePath = source.path;
break;
case ArchiveSourceType.url:
summarySlice.archiveUrl = archive.url;
summarySlice.archiveUrl = source.url;
break;
case ArchiveSourceType.build:
summarySlice.formattedBuild = formatSubmissionBuildSummary(archive.build!);
case ArchiveSourceType.buildId:
case ArchiveSourceType.latest:
summarySlice.formattedBuild = formatSubmissionBuildSummary(build!);
break;
}
return summarySlice;
+2 -11
View File
@@ -272,24 +272,15 @@ export async function ensureEASUpdatesIsConfiguredAsync(
exp: ExpoConfig;
projectId: string;
projectDir: string;
platform: RequestedPlatform | null;
platform: RequestedPlatform;
}
): Promise<void> {
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(
projectDir,
expWithoutUpdates.sdkVersion
);
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, expWithoutUpdates);
if (!hasExpoUpdates) {
await installExpoUpdatesAsync(projectDir, { silent: !Log.isDebug });
Log.withTick('Installed expo updates');
}
// Bail out if using a platform that doesn't require runtime versions
// or native setup, i.e. web.
if (!platform) {
return;
}
const workflows = await resolveWorkflowPerPlatformAsync(projectDir);
const { projectChanged, exp: expWithUpdates } =
await ensureEASUpdatesIsConfiguredInExpoConfigAsync({
+49 -24
View File
@@ -1,29 +1,37 @@
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 { SignedUrl, UploadSessionMutation } from './graphql/mutations/UploadSessionMutation';
import { PresignedPost, UploadSessionMutation } from './graphql/mutations/UploadSessionMutation';
import { ProgressHandler } from './utils/progress';
export interface PresignedPost {
url: string;
fields: Record<string, string>;
}
export async function uploadFileAtPathToGCSAsync(
export async function uploadFileAtPathToS3Async(
graphqlClient: ExpoGraphqlClient,
type: UploadSessionType,
path: string,
handleProgressEvent: ProgressHandler
): Promise<string> {
const signedUrl = await UploadSessionMutation.createUploadSessionAsync(graphqlClient, type);
): Promise<{ url: string; bucketKey: string }> {
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(graphqlClient, type);
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
await uploadWithSignedUrlWithProgressAsync(path, signedUrl, handleProgressEvent);
return signedUrl.bucketKey;
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 };
}
export async function uploadWithPresignedPostWithRetryAsync(
@@ -64,10 +72,10 @@ export async function uploadWithPresignedPostWithRetryAsync(
);
}
async function uploadWithPresignedPostAsync(
async function createPresignedPostFormDataAsync(
file: string,
presignedPost: PresignedPost
): Promise<Response> {
): Promise<{ form: FormData; fileSize: number }> {
const fileStat = await fs.stat(file);
const fileSize = fileStat.size;
const form = new FormData();
@@ -75,6 +83,14 @@ async function uploadWithPresignedPostAsync(
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',
@@ -85,25 +101,23 @@ async function uploadWithPresignedPostAsync(
});
}
async function uploadWithSignedUrlWithProgressAsync(
async function uploadWithPresignedPostWithProgressAsync(
file: string,
signedUrl: SignedUrl,
presignedPost: PresignedPost,
handleProgressEvent: ProgressHandler
): Promise<Response> {
const fileStat = await fs.stat(file);
const fileSize = fileStat.size;
const readStream = fs.createReadStream(file);
const uploadPromise = fetch(signedUrl.url, {
method: 'PUT',
body: readStream,
const { form, fileSize } = await createPresignedPostFormDataAsync(file, presignedPost);
const formHeaders = form.getHeaders();
const uploadPromise = fetch(presignedPost.url, {
method: 'POST',
body: form,
headers: {
...signedUrl.headers,
...formHeaders,
},
});
let currentSize = 0;
readStream.addListener('data', (chunk: Buffer) => {
form.addListener('data', (chunk: Buffer) => {
currentSize += Buffer.byteLength(chunk);
handleProgressEvent({
progress: {
@@ -122,3 +136,14 @@ async function uploadWithSignedUrlWithProgressAsync(
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();
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "2.7.1",
"version": "2.6.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.97",
"@expo/eas-build-job": "0.2.96",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "10.1.0",
+5 -3
View File
@@ -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": ["internal", "store"],
"enum": ["store", "internal"],
"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,7 +303,6 @@
"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",
@@ -311,7 +310,10 @@
"macos-big-sur-11.4-xcode-13.0",
"macos-big-sur-11.4-xcode-12.5"
],
"markdownEnumDescriptions": ["`macos-monterey-12.6-xcode-14.0`", "`macos-monterey-12.6-xcode-14.1`"]
"markdownEnumDescriptions": [
"`macos-monterey-12.4-xcode-13.4`",
"`macos-monterey-12.6-xcode-14.0`"
]
},
{
"type": "string"
-3
View File
@@ -5,8 +5,5 @@ 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
Generated
+4 -4
View File
@@ -1378,10 +1378,10 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@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==
"@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==
dependencies:
joi "^17.4.2"
semver "^7.3.5"