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
70 changed files with 810 additions and 1488 deletions

No files matched your search

-34
View File
@@ -8,49 +8,15 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add `eas build:run` support for Android. ([#1485](https://github.com/expo/eas-cli/pull/1485) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Only create a new channel when update branch is new. ([#1507](https://github.com/expo/eas-cli/pull/1507) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Better wording in eas submit. ([#1527](https://github.com/expo/eas-cli/pull/1527) by [@dsokal](https://github.com/dsokal))
- Upload project sources for EAS Build and archives for EAS Submit to GCS. ([#1524](https://github.com/expo/eas-cli/pull/1524) by [@wkozyra95](https://github.com/wkozyra95))
- Add a better error message for when an invalid `image` is set in `eas-json`. ([#1531](https://github.com/expo/eas-cli/pull/1531) by [@szdziedzic](https://github.com/szdziedzic))
- Remove internal expo id from device formatter. ([#1541](https://github.com/expo/eas-cli/pull/1541) by [@dsokal](https://github.com/dsokal))
## [2.7.1](https://github.com/expo/eas-cli/releases/tag/v2.7.1) - 2022-11-16
### 🐛 Bug fixes
- Use envs from the build profile to resolve credentials in `eas credentials`. ([#1520](https://github.com/expo/eas-cli/pull/1520) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Print prompt message when failing because of the non-interactive shell. ([#1523](https://github.com/expo/eas-cli/pull/1523) by [@wkozyra95](https://github.com/wkozyra95))
- Add SDK version to the analytics build events. ([#1529](https://github.com/expo/eas-cli/pull/1529) by [@wkozyra95](https://github.com/wkozyra95))
- Add Xcode 14.1 image to `eas-json`. ([#1511](https://github.com/expo/eas-cli/pull/1511) by [@szdziedzic](https://github.com/szdziedzic))
## [2.7.0](https://github.com/expo/eas-cli/releases/tag/v2.7.0) - 2022-11-11
### 🎉 New features
- Support uploading different platform combinations in `eas update`. ([#1461](https://github.com/expo/eas-cli/pull/1461) by [@EvanBacon](https://github.com/EvanBacon))
- Add Git commit to EAS Update record. ([#1499](https://github.com/expo/eas-cli/pull/1499) by [@fiberjw](https://github.com/fiberjw))
- Use local CLI for `expo export` in SDK +46. ([#1474](https://github.com/expo/eas-cli/pull/1474) by [@EvanBacon](https://github.com/EvanBacon))
- Validate icon PNGs before running Android build. ([#1477](https://github.com/expo/eas-cli/pull/1477) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fixed the values of the distribution enum on the build profile schema receiving the incorrect descriptions. ([#1504](https://github.com/expo/eas-cli/pull/1504) by [@macksal](https://github.com/macksal))
- Remove hard dependency on expo package being installed to init a project. ([#1517](https://github.com/expo/eas-cli/pull/1517) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Remove friction from the initial EAS update setup. ([#1479](https://github.com/expo/eas-cli/pull/1479) by [@byCedric](https://github.com/byCedric))
- Improve choice formatting when selecting a build to submit in the `eas submit` command. ([#1502](https://github.com/expo/eas-cli/pull/1502) by [@szdziedzic](https://github.com/szdziedzic))
## [2.6.0](https://github.com/expo/eas-cli/releases/tag/v2.6.0) - 2022-10-27
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.8.0"
"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 -51
View File
@@ -127,7 +127,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -144,7 +144,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -161,7 +161,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -175,7 +175,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -225,7 +225,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -246,7 +246,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -266,7 +266,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -286,7 +286,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -309,7 +309,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -339,7 +339,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -356,7 +356,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -373,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.8.0/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`
@@ -408,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.8.0/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`
@@ -445,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.8.0/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`
@@ -492,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.8.0/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -511,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.8.0/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v2.6.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -528,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.8.0/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]`
@@ -549,7 +549,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -571,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.8.0/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`
@@ -591,7 +591,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -614,7 +614,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -633,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.8.0/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`
@@ -650,7 +650,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -664,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.8.0/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`
@@ -684,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.8.0/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`
@@ -705,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.8.0/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]`
@@ -719,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.8.0/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`
@@ -733,7 +733,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -821,7 +821,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -838,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.8.0/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`
@@ -855,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.8.0/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`
@@ -869,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.8.0/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`
@@ -883,7 +883,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -905,7 +905,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v2.8.0/packages/eas-cli/src/commands/project/init.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`
@@ -928,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.8.0/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`
@@ -946,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.8.0/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`
@@ -960,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.8.0/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`
@@ -980,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.8.0/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`
@@ -1010,7 +1010,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -1040,7 +1040,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -1048,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.8.0/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`
@@ -1079,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.8.0/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`
@@ -1101,7 +1100,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -1121,7 +1120,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -1142,7 +1141,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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]`
@@ -1162,7 +1161,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -1179,7 +1178,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -1201,7 +1200,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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`
@@ -1218,7 +1217,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v2.8.0/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.8.0",
"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.8.0",
"@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);
});
});
+3 -3
View File
@@ -160,7 +160,7 @@ export async function ensureBranchExistsAsync(
appId: string;
branchName: string;
}
): Promise<{ branchId: string; createdBranch: boolean }> {
): Promise<{ branchId: string }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
@@ -168,14 +168,14 @@ export async function ensureBranchExistsAsync(
});
const { id } = updateBranch;
return { branchId: id, createdBranch: false };
return { branchId: id };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branchId: newUpdateBranch.id, createdBranch: true };
return { branchId: newUpdateBranch.id };
} else {
throw error;
}
@@ -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.120';
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 };
}
+9 -48
View File
@@ -2,11 +2,11 @@ import chalk from 'chalk';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildFilter, BuildFragment } from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log from '../log';
import { appPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { fromNow } from '../utils/date';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
@@ -55,51 +55,18 @@ export async function listAndRenderBuildsOnAppAsync(
}
}
function formatBuildChoiceValue(value: string | undefined | null): string {
return value ? chalk.bold(value) : chalk.dim('Unknown');
}
function formatBuildChoiceTitleAndDescription(build: BuildFragment): {
title: string;
description: string;
} {
const splitCommitMessage = build.gitCommitMessage?.split('\n');
const formattedCommitData =
build.gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
? `${chalk.dim(build.gitCommitHash.slice(0, 7))} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: 'Unknown';
return {
title: `ID: ${chalk.dim(build.id)} (${chalk.dim(`${fromNow(new Date(build.updatedAt))} ago`)})`,
description: [
`\tVersion: ${formatBuildChoiceValue(build.appVersion)}`,
`\t${
build.platform === AppPlatform.Ios ? 'Build number' : 'Version code'
}: ${formatBuildChoiceValue(build.appBuildVersion)}`,
`\tCommit: ${formattedCommitData}`,
].join('\n'),
};
}
export async function listAndSelectBuildsOnAppAsync(
graphqlClient: ExpoGraphqlClient,
selectedPlatform: AppPlatform,
{
projectId,
projectDisplayName,
filter,
queryOptions,
selectPromptDisabledFunction,
warningMessage,
}: {
projectId: string;
projectDisplayName: string;
filter?: BuildFilter;
queryOptions: PaginatedQueryOptions;
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
warningMessage?: string;
}
): Promise<BuildFragment> {
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
@@ -115,20 +82,14 @@ export async function listAndSelectBuildsOnAppAsync(
const { selectedSimulatorBuild } = await promptAsync({
type: 'select',
message: `Select ${selectedPlatform === AppPlatform.Ios ? 'iOS' : 'Android'} ${
selectedPlatform === AppPlatform.Ios ? 'simulator' : 'emulator'
} build to run for ${projectDisplayName} app`,
message: `Select simulator build to run for ${projectDisplayName} app`,
name: 'selectedSimulatorBuild',
choices: builds.map(build => {
const buildChoice = formatBuildChoiceTitleAndDescription(build);
return {
title: buildChoice.title,
description: buildChoice.description,
value: build,
disabled: selectPromptDisabledFunction?.(build),
};
}),
warn: warningMessage,
choices: builds.map(build => ({
title: `id: ${build.id}, platform: ${appPlatformDisplayNames[build.platform]}, version: ${
build.appVersion
}, build number: ${build.appBuildVersion}`,
value: build,
})),
});
return selectedSimulatorBuild;
}
@@ -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':
+19 -36
View File
@@ -1,8 +1,9 @@
import { Errors, Flags } from '@oclif/core';
import { pathExists } from 'fs-extra';
import { existsSync } from 'fs-extra';
import assert from 'node:assert';
import { getLatestBuildAsync, listAndSelectBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -10,15 +11,13 @@ import {
PaginatedQueryOptions,
getPaginatedQueryOptions,
} from '../../commandUtils/pagination';
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { RunArchiveFlags, runAsync } from '../../run/run';
import {
downloadAndMaybeExtractAppAsync,
extractAppFromLocalArchiveAsync,
} from '../../utils/download';
import { buildDistributionTypeToGraphQLDistributionType } from '../../utils/buildDistribution';
import { downloadAndExtractAppAsync, extractAppFromLocalArchiveAsync } from '../../utils/download';
interface RawRunFlags {
latest?: boolean;
@@ -52,7 +51,7 @@ export default class Run extends EasCommand {
exclusive: ['latest', 'id', 'path'],
}),
path: Flags.string({
description: 'Path to the simulator/emulator build archive or app',
description: 'Path to the simulator/emulator build archive or simulator build app',
exclusive: ['latest', 'id', 'url'],
}),
id: Flags.string({
@@ -98,19 +97,13 @@ export default class Run extends EasCommand {
const selectedPlatform = await resolvePlatformAsync(platform);
if (platform === 'ios' && process.platform !== 'darwin') {
Errors.error('You can only use an iOS simulator to run apps on macOS devices', {
exit: 1,
});
}
if (
runArchiveFlags.path &&
!(
(runArchiveFlags.path.endsWith('.tar.gz') ||
runArchiveFlags.path.endsWith('.app') ||
runArchiveFlags.path.endsWith('.apk')) &&
(await pathExists(runArchiveFlags.path))
existsSync(runArchiveFlags.path)
)
) {
Errors.error('The path must point to a .tar.gz archive, .apk file, or .app directory', {
@@ -128,10 +121,6 @@ export default class Run extends EasCommand {
}
async function resolvePlatformAsync(platform?: string): Promise<AppPlatform> {
if (process.platform !== 'darwin') {
return AppPlatform.Android;
}
if (platform && Object.values(AppPlatform).includes(platform.toUpperCase() as AppPlatform)) {
return platform.toUpperCase() as AppPlatform;
}
@@ -154,9 +143,6 @@ async function maybeGetBuildAsync(
projectId: string,
paginatedQueryOptions: PaginatedQueryOptions
): Promise<BuildFragment | null> {
const distributionType =
flags.selectedPlatform === AppPlatform.Ios ? DistributionType.Simulator : undefined;
if (flags.runArchiveFlags.id) {
return BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
} else if (
@@ -165,27 +151,26 @@ async function maybeGetBuildAsync(
!flags.runArchiveFlags.url &&
!flags.runArchiveFlags.latest
) {
return await listAndSelectBuildsOnAppAsync(graphqlClient, flags.selectedPlatform, {
return await listAndSelectBuildsOnAppAsync(graphqlClient, {
projectId,
projectDisplayName: await getDisplayNameForProjectIdAsync(graphqlClient, projectId),
filter: {
platform: flags.selectedPlatform,
distribution: distributionType,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
status: BuildStatus.Finished,
},
queryOptions: paginatedQueryOptions,
selectPromptDisabledFunction: build =>
build.platform === AppPlatform.Ios
? false
: !build.artifacts?.applicationArchiveUrl?.endsWith('.apk') ?? false,
warningMessage: 'This is not a simulator/emulator build',
});
} else if (flags.runArchiveFlags.latest) {
return await getLatestBuildAsync(graphqlClient, {
projectId,
filter: {
platform: flags.selectedPlatform,
distribution: distributionType,
distribution: buildDistributionTypeToGraphQLDistributionType(
BuildDistributionType.SIMULATOR
),
status: BuildStatus.Finished,
},
});
@@ -201,27 +186,25 @@ async function getPathToSimulatorBuildAppAsync(
queryOptions: PaginatedQueryOptions
): Promise<string> {
const maybeBuild = await maybeGetBuildAsync(graphqlClient, flags, projectId, queryOptions);
const appExtension = flags.selectedPlatform === AppPlatform.Ios ? 'app' : 'apk';
if (maybeBuild) {
if (!maybeBuild.artifacts?.applicationArchiveUrl) {
throw new Error('Build does not have an application archive url');
}
return await downloadAndMaybeExtractAppAsync(
return await downloadAndExtractAppAsync(
maybeBuild.artifacts.applicationArchiveUrl,
flags.selectedPlatform
appExtension
);
}
if (flags.runArchiveFlags.url) {
return await downloadAndMaybeExtractAppAsync(flags.runArchiveFlags.url, flags.selectedPlatform);
return await downloadAndExtractAppAsync(flags.runArchiveFlags.url, appExtension);
}
if (flags.runArchiveFlags.path?.endsWith('.tar.gz')) {
return await extractAppFromLocalArchiveAsync(
flags.runArchiveFlags.path,
flags.selectedPlatform
);
return await extractAppFromLocalArchiveAsync(flags.runArchiveFlags.path!, appExtension);
}
// this should never fail, due to the validation in sanitizeFlagsAsync
@@ -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({
+59 -103
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,37 +369,29 @@ 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, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
}
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const gitCommitHash = await getVcsClient().getCommitHashAsync();
// 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,
@@ -446,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,
@@ -517,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(
@@ -542,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
@@ -604,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.'
@@ -626,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
) {}
@@ -23,12 +23,13 @@ function formatDeviceClass(device: Device | NewDevice): string {
export default function formatDevice(device: Device, team?: AppleTeamIdAndName): string {
const fields: FormatFieldsItem[] = [
{ label: 'UDID', value: device.identifier },
{ label: 'ID', value: device.id },
{ label: 'Name', value: device.name ?? 'Unknown' },
{
label: 'Class',
value: formatDeviceClass(device),
},
{ label: 'UDID', value: device.identifier },
];
if (team) {
+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
@@ -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,
},
+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() {
-57
View File
@@ -1,57 +0,0 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import glob from 'fast-glob';
import path from 'path';
import Log from '../../log';
import { getAndroidSdkRootAsync } from './sdk';
async function aaptAsync(...options: string[]): Promise<SpawnResult> {
try {
return await spawnAsync(await getAaptExecutableAsync(), options);
} catch (error: any) {
if (error.stderr) {
Log.error(error.stderr);
}
throw error;
}
}
export async function getAaptExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (!sdkRoot) {
Log.debug('Failed to resolve the Android SDK path, falling back to global aapt executable');
return 'aapt';
}
const aaptPaths = await glob(
path.posix.join('build-tools/**', process.platform === 'win32' ? 'aapt.exe' : 'aapt'),
{ cwd: sdkRoot, absolute: true }
);
if (aaptPaths.length === 0) {
throw new Error('Failed to resolve the Android aapt path');
}
const sorted = aaptPaths.sort();
return sorted[sorted.length - 1];
}
export async function getAptParametersAsync(
appPath: string
): Promise<{ packageName: string; activityName: string }> {
const { stdout } = await aaptAsync('dump', 'badging', appPath);
const packageNameMatch = stdout.match(/package: name='([^']+)'/);
if (!packageNameMatch) {
throw new Error(`Could not read package name from ${appPath}`);
}
// get activity name
const activityNameMatch = stdout.match(/launchable-activity: name='([^']+)'/);
if (!activityNameMatch) {
throw new Error(`Could not read activity name from ${appPath}`);
}
return {
packageName: packageNameMatch[1],
activityName: activityNameMatch[1],
};
}
-132
View File
@@ -1,132 +0,0 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import os from 'os';
import path from 'path';
import Log from '../../log';
import { truthy } from '../../utils/expodash/filter';
import { sleepAsync } from '../../utils/promise';
import { getAndroidSdkRootAsync } from './sdk';
export interface AndroidEmulator {
pid?: string;
name: string;
}
const BEGINNING_OF_ADB_ERROR_MESSAGE = 'error: ';
export async function adbAsync(...args: string[]): Promise<SpawnResult> {
const adbExecutable = await getAdbExecutableAsync();
try {
return await spawnAsync(adbExecutable, args);
} catch (error: any) {
let errorMessage = (error.stderr || error.stdout || error.message).trim();
if (errorMessage.startsWith(BEGINNING_OF_ADB_ERROR_MESSAGE)) {
errorMessage = errorMessage.substring(BEGINNING_OF_ADB_ERROR_MESSAGE.length);
}
error.message = errorMessage;
throw error;
}
}
export async function getAdbExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (!sdkRoot) {
Log.debug('Failed to resolve the Android SDK path, falling back to global adb executable');
return 'adb';
}
return path.join(sdkRoot, 'platform-tools/adb');
}
export function sanitizeAdbDeviceName(deviceName: string): string | undefined {
return deviceName.trim().split(/[\r\n]+/)[0];
}
/**
* Return the Emulator name for an emulator ID, this can be used to determine if an emulator is booted.
*
* @param devicePid a value like `emulator-5554` from `abd devices`
*/
export async function getAdbNameForDeviceIdAsync(emulatorPid: string): Promise<string | null> {
const { stdout } = await adbAsync('-s', emulatorPid, 'emu', 'avd', 'name');
if (stdout.match(/could not connect to TCP port .*: Connection refused/)) {
// Can also occur when the emulator does not exist.
throw new Error(`Emulator not found: ${stdout}`);
}
return sanitizeAdbDeviceName(stdout) ?? null;
}
// TODO: This is very expensive for some operations.
export async function getRunningEmulatorsAsync(): Promise<AndroidEmulator[]> {
const { stdout } = await adbAsync('devices', '-l');
const splitItems = stdout.trim().split(os.EOL);
const attachedDevices: {
pid: string;
type: string;
}[] = splitItems
// First line is `"List of devices attached"`, remove it
.slice(1, splitItems.length)
.map(line => {
// unauthorized: ['FA8251A00719', 'unauthorized', 'usb:338690048X', 'transport_id:5']
// authorized: ['FA8251A00719', 'device', 'usb:336592896X', 'product:walleye', 'model:Pixel_2', 'device:walleye', 'transport_id:4']
// emulator: ['emulator-5554', 'offline', 'transport_id:1']
const [pid] = line.split(' ').filter(truthy);
const type = line.includes('emulator') ? 'emulator' : 'device';
return { pid, type };
})
.filter(({ pid, type }) => !!pid && type === 'emulator');
const devicePromises = attachedDevices.map<Promise<AndroidEmulator>>(async ({ pid }) => {
const name = (await getAdbNameForDeviceIdAsync(pid)) ?? '';
return {
pid,
name,
};
});
return Promise.all(devicePromises);
}
export async function getFirstRunningEmulatorAsync(): Promise<AndroidEmulator | null> {
const emulators = await getRunningEmulatorsAsync();
return emulators[0] ?? null;
}
/**
* Returns true if emulator is booted
*
* @param emulatorPid
*/
export async function isEmulatorBootedAsync(emulatorPid: string): Promise<boolean> {
try {
const { stdout } = await adbAsync('-s', emulatorPid, 'shell', 'getprop', 'sys.boot_completed');
if (stdout.trim() === '1') {
return true;
}
return false;
} catch {
return false;
}
}
export async function waitForEmulatorToBeBootedAsync(
maxWaitTimeMs: number,
intervalMs: number
): Promise<AndroidEmulator> {
Log.newLine();
Log.log('Waiting for the Android emulator to start...');
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTimeMs) {
const emulator = await getFirstRunningEmulatorAsync();
if (emulator?.pid && (await isEmulatorBootedAsync(emulator.pid))) {
return emulator;
}
await sleepAsync(intervalMs);
}
throw new Error('Timed out waiting for the Android emulator to start.');
}
@@ -1,160 +0,0 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import chalk from 'chalk';
import assert from 'node:assert';
import os from 'os';
import path from 'path';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { truthy } from '../../utils/expodash/filter';
import {
AndroidEmulator,
adbAsync,
getFirstRunningEmulatorAsync,
isEmulatorBootedAsync,
waitForEmulatorToBeBootedAsync,
} from './adb';
import { getAndroidSdkRootAsync } from './sdk';
export const EMULATOR_MAX_WAIT_TIMEOUT_MS = 60 * 1000 * 3;
export async function getEmulatorExecutableAsync(): Promise<string> {
const sdkRoot = await getAndroidSdkRootAsync();
if (sdkRoot) {
return path.join(sdkRoot, 'emulator', 'emulator');
}
return 'emulator';
}
async function emulatorAsync(...options: string[]): Promise<SpawnResult> {
const emulatorExecutable = await getEmulatorExecutableAsync();
try {
return await spawnAsync(emulatorExecutable, options);
} catch (error: any) {
if (error.stderr) {
Log.error(error.stderr);
}
throw error;
}
}
async function getAvaliableAndroidEmulatorsAsync(): Promise<AndroidEmulator[]> {
try {
const { stdout } = await emulatorAsync('-list-avds');
return stdout
.split(os.EOL)
.filter(truthy)
.map(name => ({
name,
}));
} catch {
return [];
}
}
/** Start an Android device and wait until it is booted. */
async function bootEmulatorAsync(
emulator: AndroidEmulator,
{
timeout = EMULATOR_MAX_WAIT_TIMEOUT_MS,
interval = 1000,
}: {
/** Time in milliseconds to wait before asserting a timeout error. */
timeout?: number;
interval?: number;
} = {}
): Promise<AndroidEmulator> {
Log.newLine();
Log.log(`Opening emulator ${chalk.bold(emulator.name)}`);
const emulatorExecutable = await getEmulatorExecutableAsync();
// Start a process to open an emulator
const emulatorProcess = spawnAsync(emulatorExecutable, [`@${emulator.name}`], {
stdio: 'ignore',
detached: true,
});
// we don't want to wait for the emulator process to exit before we can finish `eas build:run` command
// https://github.com/expo/eas-cli/pull/1485#discussion_r1007935871
emulatorProcess.child.unref();
return await waitForEmulatorToBeBootedAsync(timeout, interval);
}
export async function selectEmulatorAsync(): Promise<AndroidEmulator> {
const runningEmulator = await getFirstRunningEmulatorAsync();
if (runningEmulator) {
Log.newLine();
Log.log(`Using open emulator: ${chalk.bold(runningEmulator.name)}`);
return runningEmulator;
}
const emulators = await getAvaliableAndroidEmulatorsAsync();
Log.newLine();
const { selectedEmulator } = await promptAsync({
type: 'select',
message: `Select an emulator to run your app on`,
name: 'selectedEmulator',
choices: emulators.map(emulator => ({
title: emulator.name,
value: emulator,
})),
});
return selectedEmulator;
}
export async function ensureEmulatorBootedAsync(
emulator: AndroidEmulator
): Promise<AndroidEmulator> {
if (!emulator.pid || !(await isEmulatorBootedAsync(emulator.pid))) {
return await bootEmulatorAsync(emulator);
}
return emulator;
}
export async function installAppAsync(
emulator: AndroidEmulator,
apkFilePath: string
): Promise<void> {
Log.newLine();
Log.log('Installing your app...');
assert(emulator.pid);
await adbAsync('-s', emulator.pid, 'install', '-r', '-d', apkFilePath);
Log.succeed('Successfully installed your app!');
}
export async function startAppAsync(
emulator: AndroidEmulator,
packageName: string,
activityName: string
): Promise<void> {
Log.newLine();
Log.log('Starting your app...');
assert(emulator.pid);
await adbAsync(
'-s',
emulator.pid,
'shell',
'am',
'start',
'-a',
'android.intent.action.RUN',
'-f',
'0x20000000', // FLAG_ACTIVITY_SINGLE_TOP -- If set, the activity will not be launched if it is already running at the top of the history stack.
'-n',
`${packageName}/${activityName}`
);
Log.succeed('Successfully started your app!');
}
+3 -10
View File
@@ -1,12 +1,5 @@
import { getAptParametersAsync } from './aapt';
import * as emulator from './emulator';
import { assertExecutablesExistAsync } from './systemRequirements';
import Log from '../../log';
export async function runAppOnAndroidEmulatorAsync(appPath: string): Promise<void> {
await assertExecutablesExistAsync();
const selectedEmulator = await emulator.selectEmulatorAsync();
const bootedEmulator = await emulator.ensureEmulatorBootedAsync(selectedEmulator);
await emulator.installAppAsync(bootedEmulator, appPath);
const { packageName, activityName } = await getAptParametersAsync(appPath);
await emulator.startAppAsync(bootedEmulator, packageName, activityName);
export async function runAppOnAndroidEmulatorAsync(_appPath: string): Promise<void> {
Log.error('Not implemented!');
}
-26
View File
@@ -1,26 +0,0 @@
import { pathExists } from 'fs-extra';
import os from 'os';
import path from 'path';
export const ANDROID_DEFAULT_LOCATION: Readonly<Partial<Record<NodeJS.Platform, string>>> = {
darwin: path.join(os.homedir(), 'Library', 'Android', 'sdk'),
linux: path.join(os.homedir(), 'Android', 'Sdk'),
win32: path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
};
const ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM = ANDROID_DEFAULT_LOCATION[process.platform];
export async function getAndroidSdkRootAsync(): Promise<string | null> {
if (process.env.ANDROID_HOME && (await pathExists(process.env.ANDROID_HOME))) {
return process.env.ANDROID_HOME;
} else if (process.env.ANDROID_SDK_ROOT && (await pathExists(process.env.ANDROID_SDK_ROOT))) {
return process.env.ANDROID_SDK_ROOT;
} else if (
ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM &&
(await pathExists(ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM))
) {
return ANDROID_DEFAULT_LOCATION_FOR_CURRENT_PLATFORM;
} else {
return null;
}
}
@@ -1,27 +0,0 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import { getAaptExecutableAsync } from './aapt';
import { getAdbExecutableAsync } from './adb';
import { getEmulatorExecutableAsync } from './emulator';
async function assertExecutableExistsAsync(executable: string, options?: string[]): Promise<void> {
try {
await spawnAsync(executable, options);
} catch (err: any) {
throw new Error(
`${chalk.bold(
executable
)} executable doesn't seem to work. Please make sure Android Studio is installed on your device and ${chalk.bold(
'ANDROID_HOME'
)} or ${chalk.bold('ANDROID_SDK_ROOT')} env variables are set.
${err.message}`
);
}
}
export async function assertExecutablesExistAsync(): Promise<void> {
await assertExecutableExistsAsync(await getAdbExecutableAsync(), ['--version']);
await assertExecutableExistsAsync(await getEmulatorExecutableAsync(), ['-list-avds']);
await assertExecutableExistsAsync(await getAaptExecutableAsync(), ['version']);
}
+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();
}
+33 -48
View File
@@ -1,4 +1,5 @@
import spawnAsync from '@expo/spawn-async';
import cliProgress from 'cli-progress';
import glob from 'fast-glob';
import fs from 'fs';
import path from 'path';
@@ -8,18 +9,15 @@ import { promisify } from 'util';
import { v4 as uuidv4 } from 'uuid';
import fetch, { RequestInit } from '../fetch';
import { AppPlatform } from '../graphql/generated';
import Log from '../log';
import { formatBytes } from './files';
import { getTmpDirectory } from './paths';
import { ProgressHandler, createProgressTracker } from './progress';
const pipeline = promisify(Stream.pipeline);
let didProgressBarFinish = false;
type ProgressCallback = (progress: number, total: number, loaded: number) => void;
function wrapFetchWithProgress() {
return async (url: string, init: RequestInit, progressHandler: ProgressHandler) => {
return async (url: string, init: RequestInit, onProgressCallback: ProgressCallback) => {
const response = await fetch(url, init);
if (response.ok) {
@@ -42,15 +40,7 @@ function wrapFetchWithProgress() {
const progress = length / total;
if (!didProgressBarFinish) {
progressHandler({
progress: { total, percent: progress, transferred: length },
isComplete: total === length,
});
if (total === length) {
didProgressBarFinish = true;
}
}
onProgressCallback(progress, total, length);
};
response.body.on('data', chunk => {
@@ -66,23 +56,35 @@ function wrapFetchWithProgress() {
};
}
async function downloadFileWithProgressTrackerAsync(
async function downloadFileWithProgressBarAsync(
url: string,
outputPath: string,
progressTrackerMessage: string | ((ratio: number, total: number) => string),
progressTrackerCompletedMessage: string
infoMessage?: string
): Promise<void> {
Log.newLine();
Log.log(infoMessage ? infoMessage : `Downloading file from ${url}...`);
const downloadProgressBar = new cliProgress.SingleBar(
{ format: '|{bar}|' },
cliProgress.Presets.rect
);
let downloadProgressBarStarted = false;
const response = await wrapFetchWithProgress()(
url,
{
timeout: 1000 * 60 * 5, // 5 minutes
},
createProgressTracker({
message: progressTrackerMessage,
completedMessage: progressTrackerCompletedMessage,
})
(_progress: number, total: number, loaded: number): void => {
if (!downloadProgressBarStarted) {
downloadProgressBar.start(total, loaded);
downloadProgressBarStarted = true;
} else if (loaded < total) {
downloadProgressBar.update(loaded);
} else {
downloadProgressBar.stop();
}
}
);
if (!response.ok) {
@@ -92,51 +94,34 @@ async function downloadFileWithProgressTrackerAsync(
await pipeline(response.body, fs.createWriteStream(outputPath));
}
export async function downloadAndMaybeExtractAppAsync(
export async function downloadAndExtractAppAsync(
url: string,
platform: AppPlatform
applicationExtension: string
): Promise<string> {
const outputDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(outputDir, { recursive: true });
if (url.endsWith('apk')) {
const apkFilePath = path.join(outputDir, `${uuidv4()}.apk`);
await downloadFileWithProgressTrackerAsync(
url,
apkFilePath,
(ratio, total) => `Downloading app (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
'Successfully downloaded app'
);
return apkFilePath;
} else {
const tmpArchivePathDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(tmpArchivePathDir, { recursive: true });
const tmpArchivePathDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(tmpArchivePathDir, { recursive: true });
const tmpArchivePath = path.join(tmpArchivePathDir, `${uuidv4()}.tar.gz`);
const tmpArchivePath = path.join(tmpArchivePathDir, `${uuidv4()}.tar.gz`);
await downloadFileWithProgressTrackerAsync(
url,
tmpArchivePath,
(ratio, total) =>
`Downloading app archive (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
'Successfully downloaded app archive'
);
await tarExtractAsync(tmpArchivePath, outputDir);
await downloadFileWithProgressBarAsync(url, tmpArchivePath, 'Downloading app archive...');
await tarExtractAsync(tmpArchivePath, outputDir);
return await getAppPathAsync(outputDir, platform === AppPlatform.Ios ? 'app' : 'apk');
}
return await getAppPathAsync(outputDir, applicationExtension);
}
export async function extractAppFromLocalArchiveAsync(
appArchivePath: string,
platform: AppPlatform
applicationExtension: string
): Promise<string> {
const outputDir = path.join(getTmpDirectory(), uuidv4());
await fs.promises.mkdir(outputDir, { recursive: true });
await tarExtractAsync(appArchivePath, outputDir);
return await getAppPathAsync(outputDir, platform === AppPlatform.Android ? 'apk' : 'app');
return await getAppPathAsync(outputDir, applicationExtension);
}
async function getAppPathAsync(outputDir: string, applicationExtension: string): Promise<string> {
+5 -7
View File
@@ -21,7 +21,7 @@ export function createProgressTracker({
completedMessage,
}: {
total?: number;
message: string | ((ratio: number, total: number) => string);
message: string | ((ratio: number) => string);
completedMessage: string | ((duration: string) => string);
}): ProgressHandler {
let bar: Ora | null = null;
@@ -31,19 +31,17 @@ export function createProgressTracker({
const timerLabel = String(Date.now());
const getMessage = (v: number, total: number): string => {
const getMessage = (v: number): string => {
const ratio = Math.min(Math.max(v, 0), 1);
const percent = Math.floor(ratio * 100);
return typeof message === 'string'
? `${message} ${percent.toFixed(0)}%`
: message(ratio, total);
return typeof message === 'string' ? `${message} ${percent.toFixed(0)}%` : message(ratio);
};
return ({ progress, isComplete, error }) => {
if (progress) {
if (!bar && (progress.total !== undefined || total !== undefined)) {
calcTotal = (total ?? progress.total) as number;
bar = ora(getMessage(0, calcTotal)).start();
bar = ora(getMessage(0)).start();
startTimer(timerLabel);
}
if (progress.total) {
@@ -58,7 +56,7 @@ export function createProgressTracker({
percentage = current / calcTotal;
}
bar.text = getMessage(percentage, calcTotal);
bar.text = getMessage(percentage);
}
transferredSoFar = progress.transferred;
}
+2 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "2.8.0",
"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",
@@ -14,7 +14,6 @@
"joi": "17.6.4",
"log-symbols": "4.1.0",
"semver": "7.3.8",
"terminal-link": "2.1.1",
"tslib": "2.4.0"
},
"devDependencies": {
+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"
-25
View File
@@ -3,33 +3,12 @@ import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import * as fleece from 'golden-fleece';
import { ValidationError } from 'joi';
import path from 'path';
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
import { link } from './log';
import { EasJsonSchema } from './schema';
import { EasJson } from './types';
const customErrorMessageHandlers: ((err: ValidationError) => void)[] = [
// Ask user to upgrade eas-cli version or check the docs when image is invalid.
(err: ValidationError) => {
for (const detail of err.details) {
if (detail.path[detail.path.length - 1] === 'image') {
throw new InvalidEasJsonError(
chalk.red(
`Specified build image '${
detail?.context?.value
}' is not recognized. Please update your EAS CLI and see ${link(
'https://docs.expo.dev/build-reference/infrastructure/'
)} for supported build images.`
)
);
}
}
},
];
export class EasJsonAccessor {
private easJsonPath: string;
@@ -60,10 +39,6 @@ export class EasJsonAccessor {
noDefaults: true,
});
if (error) {
for (const handler of customErrorMessageHandlers) {
handler(error);
}
const errorMessages = error.message.split('. ');
throw new InvalidEasJsonError(
`${chalk.bold('eas.json')} is not valid.\n- ${errorMessages.join('\n- ')}`
-16
View File
@@ -1,16 +0,0 @@
import chalk from 'chalk';
import terminalLink from 'terminal-link';
// link function from packages/eas-cli/src/log.ts
export function link(
url: string,
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
): string {
let output: string;
if (terminalLink.isSupported) {
output = terminalLink(text, url);
} else {
output = `${text === url ? '' : text + ': '}${chalk.underline(url)}`;
}
return dim ? chalk.dim(output) : output;
}
-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"