mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fdf47aa7d | ||
|
|
2279b650d5 | ||
|
|
c6a36e2e47 | ||
|
|
27cb8c12d6 | ||
|
|
1122a7f374 | ||
|
|
c06d18138c | ||
|
|
3f4ff1dc3e | ||
|
|
e852ab1cb7 | ||
|
|
d941398ce6 | ||
|
|
bae7684996 |
No files matched your search
@@ -8,10 +8,17 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add `eas integrations:asc` commands to manage App Store Connect integrations for EAS projects. ([#3558](https://github.com/expo/eas-cli/pull/3558) by [@sswrk](https://github.com/sswrk))
|
||||
- [eas-cli] Allow `eas build:download` to accept a build ID. ([#3655](https://github.com/expo/eas-cli/pull/3655) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [steps] Fix workflow job context interpolation when a string contains multiple `${{ }}` expressions on the same line (for example a changelog built from two context values). ([#3644](https://github.com/expo/eas-cli/pull/3644) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.8.1](https://github.com/expo/eas-cli/releases/tag/v18.8.1) - 2026-04-23
|
||||
|
||||
## [18.8.0](https://github.com/expo/eas-cli/releases/tag/v18.8.0) - 2026-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.8.1",
|
||||
"version": "18.9.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "18.8.1",
|
||||
"version": "18.9.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.8.0",
|
||||
"@expo/steps": "18.9.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "18.8.0"
|
||||
"@expo/steps": "18.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
+162
-94
@@ -117,6 +117,9 @@ eas --help COMMAND
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas init`](#eas-init)
|
||||
* [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory)
|
||||
* [`eas integrations:asc:connect`](#eas-integrationsascconnect)
|
||||
* [`eas integrations:asc:disconnect`](#eas-integrationsascdisconnect)
|
||||
* [`eas integrations:asc:status`](#eas-integrationsascstatus)
|
||||
* [`eas login`](#eas-login)
|
||||
* [`eas logout`](#eas-logout)
|
||||
* [`eas metadata:lint`](#eas-metadatalint)
|
||||
@@ -180,7 +183,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -197,7 +200,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -219,7 +222,7 @@ DESCRIPTION
|
||||
view account usage and billing for the current cycle
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -236,7 +239,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -250,7 +253,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -302,7 +305,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -323,7 +326,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -343,7 +346,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -363,7 +366,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -386,7 +389,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -424,7 +427,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -444,7 +447,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -462,7 +465,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -482,7 +485,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -504,28 +507,34 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
download a simulator/emulator build by build ID or fingerprint hash
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:download --fingerprint <value> [-p ios|android] [--dev-client] [--json] [--non-interactive]
|
||||
$ eas build:download [--build-id <value> | --fingerprint <value> | -p ios|android | --dev-client]
|
||||
[--all-artifacts] [--json] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=<option> <options: ios|android>
|
||||
--all-artifacts Download all available build artifacts (build artifacts archive, Xcode logs, etc.) in
|
||||
addition to the application archive. Without this flag, only the application archive is
|
||||
downloaded and the command errors if it is missing.
|
||||
--build-id=<value> ID of the build to download. Mutually exclusive with --fingerprint, --platform, and
|
||||
--dev-client; the platform is derived from the build itself.
|
||||
--[no-]dev-client Filter only dev-client builds.
|
||||
--fingerprint=<value> (required) Fingerprint hash of the build to download
|
||||
--fingerprint=<value> Fingerprint hash of the build to download
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
download a simulator/emulator build by build ID or fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -563,7 +572,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -603,7 +612,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -633,7 +642,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -659,7 +668,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -711,7 +720,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -729,7 +738,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -747,7 +756,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -764,7 +773,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -785,7 +794,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -806,7 +815,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -828,7 +837,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:insights`
|
||||
|
||||
@@ -852,7 +861,7 @@ DESCRIPTION
|
||||
display adoption, crash, and unique-user insights for a channel + runtime version
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -872,7 +881,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -894,7 +903,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -916,7 +925,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -951,7 +960,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -974,7 +983,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -995,7 +1004,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -1012,7 +1021,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1030,7 +1039,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1059,7 +1068,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1084,7 +1093,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1105,7 +1114,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1126,7 +1135,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1163,7 +1172,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1183,7 +1192,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1204,7 +1213,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1225,7 +1234,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1239,7 +1248,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1253,7 +1262,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1285,7 +1294,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1311,7 +1320,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1333,7 +1342,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1361,7 +1370,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1389,7 +1398,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1412,7 +1421,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1434,7 +1443,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1468,7 +1477,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1512,7 +1521,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1543,7 +1552,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1602,6 +1611,65 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
## `eas integrations:asc:connect`
|
||||
|
||||
connect a project to an App Store Connect app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas integrations:asc:connect [--api-key-id <value>] [--asc-app-id <value>] [--bundle-id <value>] [--json]
|
||||
[--non-interactive]
|
||||
|
||||
FLAGS
|
||||
--api-key-id=<value> Apple App Store Connect API Key ID
|
||||
--asc-app-id=<value> App Store Connect app identifier
|
||||
--bundle-id=<value> Filter discovered apps by bundle identifier
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
connect a project to an App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
|
||||
## `eas integrations:asc:disconnect`
|
||||
|
||||
disconnect the current project from its App Store Connect app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas integrations:asc:disconnect [--yes] [--json] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--yes Skip confirmation prompt
|
||||
|
||||
DESCRIPTION
|
||||
disconnect the current project from its App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
|
||||
## `eas integrations:asc:status`
|
||||
|
||||
show the App Store Connect app link status for the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas integrations:asc:status [--json] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
show the App Store Connect app link status for the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
|
||||
## `eas login`
|
||||
|
||||
log in with your Expo account
|
||||
@@ -1652,7 +1720,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1670,7 +1738,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1688,7 +1756,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1740,7 +1808,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1754,7 +1822,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1777,7 +1845,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1801,7 +1869,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/project/new.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1819,7 +1887,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1852,7 +1920,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1895,7 +1963,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1916,7 +1984,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1937,7 +2005,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1962,7 +2030,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:insights GROUPID`
|
||||
|
||||
@@ -1989,7 +2057,7 @@ DESCRIPTION
|
||||
display launch, crash, unique-user, and size insights for an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -2016,7 +2084,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -2054,7 +2122,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2082,7 +2150,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2112,7 +2180,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2133,7 +2201,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2157,7 +2225,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2178,7 +2246,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2200,7 +2268,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2220,7 +2288,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2239,7 +2307,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2262,7 +2330,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2279,7 +2347,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2400,7 +2468,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2420,7 +2488,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2444,7 +2512,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -2484,7 +2552,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2506,7 +2574,7 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2534,7 +2602,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2554,7 +2622,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2576,5 +2644,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.8.1/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.9.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+1332
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.8.1",
|
||||
"version": "18.9.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.8.0",
|
||||
"@expo/steps": "18.9.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^4.8.3",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
@@ -201,6 +201,9 @@
|
||||
"env": {
|
||||
"description": "manage project and account environment variables"
|
||||
},
|
||||
"integrations": {
|
||||
"description": "manage service connections"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "manage store configuration"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { AppPlatform, BuildFragment } from '../../../graphql/generated';
|
||||
import { BuildQuery } from '../../../graphql/queries/BuildQuery';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
import Download from '../download';
|
||||
|
||||
jest.mock('../../../graphql/queries/BuildQuery');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../utils/json');
|
||||
|
||||
const mockByIdAsync = jest.mocked(BuildQuery.byIdAsync);
|
||||
const mockViewBuildsOnAppAsync = jest.mocked(BuildQuery.viewBuildsOnAppAsync);
|
||||
const mockEnableJsonOutput = jest.mocked(enableJsonOutput);
|
||||
const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput);
|
||||
|
||||
function makeBuild(overrides: Partial<BuildFragment> = {}): BuildFragment {
|
||||
return {
|
||||
id: 'build-123',
|
||||
platform: AppPlatform.Ios,
|
||||
project: { id: 'project-1' },
|
||||
artifacts: {
|
||||
applicationArchiveUrl: 'https://example.com/app.tar.gz',
|
||||
buildArtifactsUrl: null,
|
||||
xcodeBuildLogsUrl: null,
|
||||
buildUrl: null,
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as BuildFragment;
|
||||
}
|
||||
|
||||
describe(Download, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const projectId = 'test-project-id';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createCommand(argv: string[]): Download {
|
||||
const command = new Download(argv, mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockResolvedValue({
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
// Stub the file-system-touching helpers so we don't hit disk.
|
||||
jest
|
||||
.spyOn(command as any, 'getPathToBuildArtifactAsync')
|
||||
.mockResolvedValue('/cache/path/to/build.app');
|
||||
jest.spyOn(command as any, 'downloadExtraArtifactsAsync').mockResolvedValue({});
|
||||
return command;
|
||||
}
|
||||
|
||||
it('errors when neither --build-id nor --fingerprint is provided', async () => {
|
||||
const command = createCommand([]);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Either --build-id or --fingerprint is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches build by ID when --build-id is provided', async () => {
|
||||
mockByIdAsync.mockResolvedValue(makeBuild());
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockByIdAsync).toHaveBeenCalledWith(graphqlClient, 'build-123');
|
||||
expect(mockViewBuildsOnAppAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches builds by fingerprint when --fingerprint is provided', async () => {
|
||||
mockViewBuildsOnAppAsync.mockResolvedValue([makeBuild()]);
|
||||
|
||||
const command = createCommand(['--fingerprint', 'fp-abc', '--platform', 'ios']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockViewBuildsOnAppAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockViewBuildsOnAppAsync.mock.calls[0][1].filter?.fingerprintHash).toBe('fp-abc');
|
||||
expect(mockByIdAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses platform from the build itself when --build-id is provided', async () => {
|
||||
mockByIdAsync.mockResolvedValue(makeBuild({ platform: AppPlatform.Android }));
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await command.runAsync();
|
||||
|
||||
const getPathSpy = jest.spyOn(command as any, 'getPathToBuildArtifactAsync');
|
||||
// The spy was set up after instantiation; just ensure runAsync used the build's platform
|
||||
// by checking that no platform prompt was needed.
|
||||
expect(getPathSpy).toBeDefined();
|
||||
});
|
||||
|
||||
it('errors with a helpful message when applicationArchiveUrl is missing and other artifacts exist', async () => {
|
||||
mockByIdAsync.mockResolvedValue(
|
||||
makeBuild({
|
||||
artifacts: {
|
||||
applicationArchiveUrl: null,
|
||||
buildArtifactsUrl: 'https://example.com/build-artifacts.tar.gz',
|
||||
xcodeBuildLogsUrl: 'https://example.com/xcode-logs.tar.gz',
|
||||
buildUrl: null,
|
||||
} as any,
|
||||
})
|
||||
);
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
/Other artifacts are available \(buildArtifacts, xcodeBuildLogs\); re-run with --all-artifacts/
|
||||
);
|
||||
});
|
||||
|
||||
it('errors with the basic message when applicationArchiveUrl is missing and no other artifacts exist', async () => {
|
||||
mockByIdAsync.mockResolvedValue(
|
||||
makeBuild({
|
||||
artifacts: {
|
||||
applicationArchiveUrl: null,
|
||||
buildArtifactsUrl: null,
|
||||
xcodeBuildLogsUrl: null,
|
||||
buildUrl: null,
|
||||
} as any,
|
||||
})
|
||||
);
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123']);
|
||||
await expect(command.runAsync()).rejects.toThrow('Build does not have an application archive url');
|
||||
});
|
||||
|
||||
it('does not throw when applicationArchiveUrl is missing but --all-artifacts is set', async () => {
|
||||
mockByIdAsync.mockResolvedValue(
|
||||
makeBuild({
|
||||
artifacts: {
|
||||
applicationArchiveUrl: null,
|
||||
buildArtifactsUrl: 'https://example.com/build-artifacts.tar.gz',
|
||||
xcodeBuildLogsUrl: null,
|
||||
buildUrl: null,
|
||||
} as any,
|
||||
})
|
||||
);
|
||||
|
||||
const command = createCommand(['--build-id', 'build-123', '--all-artifacts']);
|
||||
const downloadExtrasSpy = jest
|
||||
.spyOn(command as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({ buildArtifacts: '/cache/build-123-artifacts/build-artifacts.tar.gz' });
|
||||
|
||||
await expect(command.runAsync()).resolves.not.toThrow();
|
||||
expect(downloadExtrasSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('downloads extras only when --all-artifacts is set, regardless of --build-id vs --fingerprint', async () => {
|
||||
mockViewBuildsOnAppAsync.mockResolvedValue([makeBuild()]);
|
||||
|
||||
const commandWithoutFlag = createCommand(['--fingerprint', 'fp-abc', '--platform', 'ios']);
|
||||
const downloadExtrasSpyWithout = jest
|
||||
.spyOn(commandWithoutFlag as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({});
|
||||
await commandWithoutFlag.runAsync();
|
||||
expect(downloadExtrasSpyWithout).not.toHaveBeenCalled();
|
||||
|
||||
const commandWithFlag = createCommand([
|
||||
'--fingerprint',
|
||||
'fp-abc',
|
||||
'--platform',
|
||||
'ios',
|
||||
'--all-artifacts',
|
||||
]);
|
||||
const downloadExtrasSpyWith = jest
|
||||
.spyOn(commandWithFlag as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({ buildArtifacts: '/path/to/extras' });
|
||||
await commandWithFlag.runAsync();
|
||||
expect(downloadExtrasSpyWith).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits JSON with path and extras when --json --all-artifacts are set', async () => {
|
||||
mockByIdAsync.mockResolvedValue(makeBuild());
|
||||
|
||||
const command = createCommand([
|
||||
'--build-id',
|
||||
'build-123',
|
||||
'--all-artifacts',
|
||||
'--json',
|
||||
'--non-interactive',
|
||||
]);
|
||||
jest
|
||||
.spyOn(command as any, 'downloadExtraArtifactsAsync')
|
||||
.mockResolvedValue({ buildArtifacts: '/cache/extras/build-artifacts.tar.gz' });
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockEnableJsonOutput).toHaveBeenCalled();
|
||||
expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith({
|
||||
path: '/cache/path/to/build.app',
|
||||
buildArtifacts: '/cache/extras/build-artifacts.tar.gz',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
@@ -15,24 +16,43 @@ import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getEasBuildRunCachedAppPath } from '../../run/run';
|
||||
import { downloadAndMaybeExtractAppAsync } from '../../utils/download';
|
||||
import {
|
||||
downloadAndMaybeExtractAppAsync,
|
||||
downloadFileWithProgressTrackerAsync,
|
||||
} from '../../utils/download';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getEasBuildRunCacheDirectoryPath } from '../../utils/paths';
|
||||
|
||||
export default class Download extends EasCommand {
|
||||
static override description = 'download simulator/emulator builds for a given fingerprint hash';
|
||||
static override description =
|
||||
'download a simulator/emulator build by build ID or fingerprint hash';
|
||||
|
||||
|
||||
static override flags = {
|
||||
'build-id': Flags.string({
|
||||
description:
|
||||
'ID of the build to download. Mutually exclusive with --fingerprint, --platform, and --dev-client; the platform is derived from the build itself.',
|
||||
exclusive: ['fingerprint', 'platform', 'dev-client'],
|
||||
}),
|
||||
fingerprint: Flags.string({
|
||||
description: 'Fingerprint hash of the build to download',
|
||||
required: true,
|
||||
exclusive: ['build-id'],
|
||||
}),
|
||||
platform: Flags.option({
|
||||
char: 'p',
|
||||
options: [Platform.IOS, Platform.ANDROID] as const,
|
||||
exclusive: ['build-id'],
|
||||
})(),
|
||||
'dev-client': Flags.boolean({
|
||||
description: 'Filter only dev-client builds.',
|
||||
allowNo: true,
|
||||
exclusive: ['build-id'],
|
||||
}),
|
||||
'all-artifacts': Flags.boolean({
|
||||
description:
|
||||
'Download all available build artifacts (build artifacts archive, Xcode logs, etc.) in addition to the application archive. Without this flag, only the application archive is downloaded and the command errors if it is missing.',
|
||||
default: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -45,14 +65,20 @@ export default class Download extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: {
|
||||
'build-id': buildId,
|
||||
platform,
|
||||
fingerprint,
|
||||
'dev-client': developmentClient,
|
||||
'all-artifacts': allArtifacts,
|
||||
...rawFlags
|
||||
},
|
||||
} = await this.parse(Download);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(rawFlags);
|
||||
|
||||
if (!buildId && !fingerprint) {
|
||||
throw new Error('Either --build-id or --fingerprint is required.');
|
||||
}
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
projectId,
|
||||
@@ -64,24 +90,113 @@ export default class Download extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const selectedPlatform = await resolvePlatformAsync({ nonInteractive, platform });
|
||||
const build = await this.getBuildAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform: selectedPlatform,
|
||||
fingerprintHash: fingerprint,
|
||||
developmentClient,
|
||||
});
|
||||
const buildArtifactPath = await this.getPathToBuildArtifactAsync(build, selectedPlatform);
|
||||
let build: BuildFragment;
|
||||
let selectedPlatform: AppPlatform;
|
||||
if (buildId) {
|
||||
build = await this.getBuildByIdAsync({ graphqlClient, buildId });
|
||||
selectedPlatform = build.platform;
|
||||
Log.succeed(`🎯 Found build ${chalk.bold(buildId)} on EAS servers.`);
|
||||
} else {
|
||||
selectedPlatform = await resolvePlatformAsync({ nonInteractive, platform });
|
||||
build = await this.getBuildByFingerprintAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform: selectedPlatform,
|
||||
fingerprintHash: fingerprint!,
|
||||
developmentClient,
|
||||
});
|
||||
}
|
||||
|
||||
let buildArtifactPath: string | null = null;
|
||||
if (build.artifacts?.applicationArchiveUrl) {
|
||||
buildArtifactPath = await this.getPathToBuildArtifactAsync(build, selectedPlatform);
|
||||
} else if (!allArtifacts) {
|
||||
const availableArtifacts = listAvailableExtraArtifactNames(build);
|
||||
if (availableArtifacts.length > 0) {
|
||||
throw new Error(
|
||||
`Build does not have an application archive url. Other artifacts are available (${availableArtifacts.join(', ')}); re-run with --all-artifacts to download them.`
|
||||
);
|
||||
}
|
||||
throw new Error('Build does not have an application archive url');
|
||||
}
|
||||
|
||||
let extraArtifactPaths: Record<string, string> = {};
|
||||
if (allArtifacts) {
|
||||
extraArtifactPaths = await this.downloadExtraArtifactsAsync(build);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
const jsonResults = { path: buildArtifactPath };
|
||||
const jsonResults = {
|
||||
...(buildArtifactPath != null && { path: buildArtifactPath }),
|
||||
...extraArtifactPaths,
|
||||
};
|
||||
printJsonOnlyOutput(jsonResults);
|
||||
} else {
|
||||
Log.log(`Build downloaded to ${chalk.bold(buildArtifactPath)}`);
|
||||
if (buildArtifactPath != null) {
|
||||
Log.log(`Build downloaded to ${chalk.bold(buildArtifactPath)}`);
|
||||
}
|
||||
for (const [name, filePath] of Object.entries(extraArtifactPaths)) {
|
||||
Log.log(`${name} downloaded to ${chalk.bold(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getBuildAsync({
|
||||
private async downloadExtraArtifactsAsync(build: BuildFragment): Promise<Record<string, string>> {
|
||||
const artifacts = build.artifacts;
|
||||
if (!artifacts) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const extraArtifacts: Array<{ name: string; url: string }> = [];
|
||||
if (artifacts.buildArtifactsUrl) {
|
||||
extraArtifacts.push({ name: 'buildArtifacts', url: artifacts.buildArtifactsUrl });
|
||||
}
|
||||
if (artifacts.xcodeBuildLogsUrl) {
|
||||
extraArtifacts.push({ name: 'xcodeBuildLogs', url: artifacts.xcodeBuildLogsUrl });
|
||||
}
|
||||
if (artifacts.buildUrl && artifacts.buildUrl !== artifacts.applicationArchiveUrl) {
|
||||
extraArtifacts.push({ name: 'build', url: artifacts.buildUrl });
|
||||
}
|
||||
|
||||
if (extraArtifacts.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const outputDir = path.join(getEasBuildRunCacheDirectoryPath(), `${build.id}-artifacts`);
|
||||
await fs.ensureDir(outputDir);
|
||||
|
||||
const downloaded: Record<string, string> = {};
|
||||
for (const { name, url } of extraArtifacts) {
|
||||
const fileName = getFileNameFromUrl(url, name);
|
||||
const outputPath = path.join(outputDir, fileName);
|
||||
await downloadFileWithProgressTrackerAsync(
|
||||
url,
|
||||
outputPath,
|
||||
(ratio, total) =>
|
||||
`Downloading ${name} (${formatBytes(total * ratio)} / ${formatBytes(total)})`,
|
||||
`Successfully downloaded ${name}`
|
||||
);
|
||||
downloaded[name] = outputPath;
|
||||
}
|
||||
|
||||
return downloaded;
|
||||
}
|
||||
|
||||
private async getBuildByIdAsync({
|
||||
graphqlClient,
|
||||
buildId,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
buildId: string;
|
||||
}): Promise<BuildFragment> {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(graphqlClient, buildId);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Could not find build with ID ${buildId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBuildByFingerprintAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
platform,
|
||||
@@ -108,7 +223,7 @@ export default class Download extends EasCommand {
|
||||
limit: 1,
|
||||
});
|
||||
if (builds.length === 0) {
|
||||
throw new Errors.CLIError(
|
||||
throw new Error(
|
||||
`No builds available for ${platform} with fingerprint hash ${fingerprintHash}`
|
||||
);
|
||||
}
|
||||
@@ -123,7 +238,7 @@ export default class Download extends EasCommand {
|
||||
build.id,
|
||||
platform
|
||||
);
|
||||
if (await pathExists(cachedBuildArtifactPath)) {
|
||||
if (await fs.pathExists(cachedBuildArtifactPath)) {
|
||||
Log.newLine();
|
||||
Log.log(`Using cached build...`);
|
||||
return cachedBuildArtifactPath;
|
||||
@@ -167,3 +282,33 @@ async function resolvePlatformAsync({
|
||||
});
|
||||
return selectedPlatform;
|
||||
}
|
||||
|
||||
function listAvailableExtraArtifactNames(build: BuildFragment): string[] {
|
||||
const names: string[] = [];
|
||||
if (build.artifacts?.buildArtifactsUrl) {
|
||||
names.push('buildArtifacts');
|
||||
}
|
||||
if (build.artifacts?.xcodeBuildLogsUrl) {
|
||||
names.push('xcodeBuildLogs');
|
||||
}
|
||||
if (
|
||||
build.artifacts?.buildUrl &&
|
||||
build.artifacts.buildUrl !== build.artifacts.applicationArchiveUrl
|
||||
) {
|
||||
names.push('build');
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function getFileNameFromUrl(url: string, fallbackName: string): string {
|
||||
try {
|
||||
const pathname = new URL(url).pathname;
|
||||
const basename = path.basename(pathname);
|
||||
if (basename) {
|
||||
return basename;
|
||||
}
|
||||
} catch {
|
||||
// fall through to default
|
||||
}
|
||||
return fallbackName;
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasCommandError } from '../../../../commandUtils/errors';
|
||||
import { selectOrCreateAscApiKeyIdAsync } from '../../../../integrations/asc/ascApiKey';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../../../credentials/ios/api/graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { AscAppLinkMutation } from '../../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../../graphql/queries/AscAppLinkQuery';
|
||||
import IntegrationsAscConnect from '../connect';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AscAppLinkQuery');
|
||||
jest.mock('../../../../graphql/mutations/AscAppLinkMutation');
|
||||
jest.mock('../../../../credentials/ios/api/graphql/queries/AppStoreConnectApiKeyQuery');
|
||||
jest.mock('../../../../integrations/asc/ascApiKey');
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('../../../../ora');
|
||||
|
||||
const testProjectId = 'test-project-id';
|
||||
const mockMetadataConnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
const mockRemoteApps = [
|
||||
{
|
||||
ascAppIdentifier: '9876543210',
|
||||
bundleIdentifier: 'com.test.newapp',
|
||||
name: 'New App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
];
|
||||
|
||||
describe(IntegrationsAscConnect, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const actor = { id: 'actor-id' } as any;
|
||||
const analytics = {} as any;
|
||||
const vcsClient = {} as any;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fails when already connected', async () => {
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockResolvedValueOnce(mockMetadataConnected);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'key-id', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(EasCommandError);
|
||||
});
|
||||
|
||||
it('connects successfully in non-interactive mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected)
|
||||
.mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest
|
||||
.mocked(AscAppLinkMutation.createAppStoreConnectAppAsync)
|
||||
.mockResolvedValueOnce({ id: 'new-link-id', ascAppIdentifier: '9876543210' });
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.createAppStoreConnectAppAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
ascAppIdentifier: '9876543210',
|
||||
appStoreConnectApiKeyId: 'key-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts Apple key identifier in non-interactive mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected)
|
||||
.mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest
|
||||
.mocked(AscAppLinkMutation.createAppStoreConnectAppAsync)
|
||||
.mockResolvedValueOnce({ id: 'new-link-id', ascAppIdentifier: '9876543210' });
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'eas-key-uuid',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.createAppStoreConnectAppAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
ascAppIdentifier: '9876543210',
|
||||
appStoreConnectApiKeyId: 'eas-key-uuid',
|
||||
});
|
||||
});
|
||||
|
||||
it('requires --api-key-id in non-interactive mode', async () => {
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('--api-key-id is required');
|
||||
});
|
||||
|
||||
it('requires --asc-app-id in non-interactive mode', async () => {
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('--asc-app-id is required');
|
||||
});
|
||||
|
||||
it('fails when asc-app-id is not found among discovered apps', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', 'nonexistent', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('was not found among accessible apps');
|
||||
});
|
||||
|
||||
it('fails when passing EAS key id in non-interactive mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'eas-key-uuid',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'eas-key-uuid', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'No App Store Connect API key found with Apple key identifier'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when multiple keys match Apple key identifier', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'eas-key-uuid-1',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
{
|
||||
id: 'eas-key-uuid-2',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Multiple App Store Connect API keys match Apple key identifier'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when no accessible apps are discovered', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce([]);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'No accessible apps found on App Store Connect'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when app discovery throws', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync)
|
||||
.mockRejectedValueOnce(new Error('discovery failed'));
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'key-id',
|
||||
keyIdentifier: 'FAKEKEY000',
|
||||
},
|
||||
] as any);
|
||||
|
||||
const command = new IntegrationsAscConnect(
|
||||
['--api-key-id', 'FAKEKEY000', '--asc-app-id', '9876543210', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('discovery failed');
|
||||
});
|
||||
|
||||
it('creates or selects key when no api-key-id is provided', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected)
|
||||
.mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getAllForAccountAsync).mockResolvedValueOnce([]);
|
||||
jest.mocked(selectOrCreateAscApiKeyIdAsync).mockResolvedValueOnce('generated-key-id');
|
||||
jest.mocked(AscAppLinkQuery.discoverAccessibleAppsAsync).mockResolvedValueOnce(mockRemoteApps);
|
||||
jest
|
||||
.mocked(AscAppLinkMutation.createAppStoreConnectAppAsync)
|
||||
.mockResolvedValueOnce({ id: 'new-link-id', ascAppIdentifier: '9876543210' });
|
||||
|
||||
const command = new IntegrationsAscConnect(['--asc-app-id', '9876543210'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
projectDir: '/test/project',
|
||||
analytics,
|
||||
vcsClient,
|
||||
loggedIn: { graphqlClient, actor },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(selectOrCreateAscApiKeyIdAsync).toHaveBeenCalled();
|
||||
expect(AscAppLinkMutation.createAppStoreConnectAppAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
ascAppIdentifier: '9876543210',
|
||||
appStoreConnectApiKeyId: 'generated-key-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AscAppLinkMutation } from '../../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../../log';
|
||||
import { toggleConfirmAsync } from '../../../../prompts';
|
||||
import IntegrationsAscDisconnect from '../disconnect';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AscAppLinkQuery');
|
||||
jest.mock('../../../../graphql/mutations/AscAppLinkMutation');
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../../../prompts');
|
||||
|
||||
const testProjectId = 'test-project-id';
|
||||
const mockMetadataConnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
describe(IntegrationsAscDisconnect, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('disconnects successfully with --yes', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataConnected)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
jest.mocked(AscAppLinkMutation.deleteAppStoreConnectAppAsync).mockResolvedValueOnce(undefined);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
'asc-app-link-id'
|
||||
);
|
||||
expect(AscAppLinkQuery.getAppMetadataAsync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
graphqlClient,
|
||||
testProjectId,
|
||||
{
|
||||
useCache: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('no-op when already disconnected', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prints json output when already disconnected in json mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes', '--json'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"action": "disconnect"')
|
||||
);
|
||||
});
|
||||
|
||||
it('handles invalid ASC API key gracefully', async () => {
|
||||
const ascError = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockRejectedValueOnce(ascError);
|
||||
|
||||
const command = new IntegrationsAscDisconnect(['--yes'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
expect(jest.mocked(Log.warn)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('revoked or is no longer valid')
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels disconnection when confirmation is rejected', async () => {
|
||||
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
|
||||
throw new Error('process.exit called');
|
||||
}) as never);
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockResolvedValueOnce(mockMetadataConnected);
|
||||
jest.mocked(toggleConfirmAsync).mockResolvedValueOnce(false);
|
||||
|
||||
const command = new IntegrationsAscDisconnect([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('process.exit called');
|
||||
expect(AscAppLinkMutation.deleteAppStoreConnectAppAsync).not.toHaveBeenCalled();
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import { getMockOclifConfig } from '../../../../__tests__/commands/utils';
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AscAppLinkQuery } from '../../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../../log';
|
||||
import IntegrationsAscStatus from '../status';
|
||||
|
||||
jest.mock('../../../../graphql/queries/AscAppLinkQuery');
|
||||
jest.mock('../../../../log');
|
||||
jest.mock('../../../../ora');
|
||||
|
||||
const testProjectId = 'test-project-id';
|
||||
const mockMetadataConnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: testProjectId,
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
describe(IntegrationsAscStatus, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('displays connected status', async () => {
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockResolvedValueOnce(mockMetadataConnected);
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(AscAppLinkQuery.getAppMetadataAsync).toHaveBeenCalledWith(graphqlClient, testProjectId);
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays disconnected status', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prints json output in json mode', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockResolvedValueOnce(mockMetadataDisconnected);
|
||||
|
||||
const command = new IntegrationsAscStatus(['--json'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"action": "status"')
|
||||
);
|
||||
});
|
||||
|
||||
it('handles invalid ASC API key gracefully', async () => {
|
||||
const ascError = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockRejectedValueOnce(ascError);
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.warn)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('revoked or is no longer valid')
|
||||
);
|
||||
});
|
||||
|
||||
it('handles invalid ASC API key with json output', async () => {
|
||||
const ascError = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
jest.mocked(AscAppLinkQuery.getAppMetadataAsync).mockRejectedValueOnce(ascError);
|
||||
|
||||
const command = new IntegrationsAscStatus(['--json'], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
expect(jest.mocked(Log.log)).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"status": "invalid"')
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when fetching status fails', async () => {
|
||||
jest
|
||||
.mocked(AscAppLinkQuery.getAppMetadataAsync)
|
||||
.mockRejectedValueOnce(new Error('status failed'));
|
||||
|
||||
const command = new IntegrationsAscStatus([], mockConfig);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
projectId: testProjectId,
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow('status failed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { EasCommandError } from '../../../commandUtils/errors';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import { CredentialsContext } from '../../../credentials/context';
|
||||
import { buildJsonOutput, formatAscAppLinkStatus } from '../../../integrations/asc/utils';
|
||||
import { selectOrCreateAscApiKeyIdAsync } from '../../../integrations/asc/ascApiKey';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../../credentials/ios/api/graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { AscAppLinkMutation } from '../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { selectAsync } from '../../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
|
||||
type DiscoveredAscApps = Awaited<ReturnType<typeof AscAppLinkQuery.discoverAccessibleAppsAsync>>;
|
||||
|
||||
export default class IntegrationsAscConnect extends EasCommand {
|
||||
static override description = 'connect a project to an App Store Connect app';
|
||||
|
||||
static override flags = {
|
||||
'api-key-id': Flags.string({
|
||||
description: 'Apple App Store Connect API Key ID',
|
||||
}),
|
||||
'asc-app-id': Flags.string({
|
||||
description: 'App Store Connect app identifier',
|
||||
}),
|
||||
'bundle-id': Flags.string({
|
||||
description: 'Filter discovered apps by bundle identifier',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
...this.ContextOptions.Vcs,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(IntegrationsAscConnect);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
if (!flags['api-key-id']) {
|
||||
throw new EasCommandError('--api-key-id is required in non-interactive mode.');
|
||||
}
|
||||
if (!flags['asc-app-id']) {
|
||||
throw new EasCommandError('--asc-app-id is required in non-interactive mode.');
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
projectDir,
|
||||
loggedIn: { actor, graphqlClient },
|
||||
analytics,
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(IntegrationsAscConnect, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
// Step 1: Check current status
|
||||
const statusSpinner = ora('Checking current App Store Connect app link status').start();
|
||||
const metadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
statusSpinner.succeed('Checked current status');
|
||||
|
||||
if (metadata.appStoreConnectApp) {
|
||||
throw new EasCommandError(
|
||||
`Project ${chalk.bold(metadata.fullName)} is already connected to App Store Connect app ${chalk.bold(metadata.appStoreConnectApp.ascAppIdentifier)}. Disconnect first with ${chalk.bold('eas integrations:asc:disconnect')}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Get ASC API key
|
||||
const keysSpinner = ora('Fetching App Store Connect API keys').start();
|
||||
const keys = await AppStoreConnectApiKeyQuery.getAllForAccountAsync(
|
||||
graphqlClient,
|
||||
metadata.ownerAccount.name
|
||||
);
|
||||
keysSpinner.succeed(`Found ${keys.length} API key(s)`);
|
||||
|
||||
let apiKeyId = flags['api-key-id'];
|
||||
if (!apiKeyId) {
|
||||
const credentialsContext = new CredentialsContext({
|
||||
projectInfo: null,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
});
|
||||
|
||||
apiKeyId = await selectOrCreateAscApiKeyIdAsync({
|
||||
credentialsContext,
|
||||
existingKeys: keys,
|
||||
ownerAccount: metadata.ownerAccount,
|
||||
});
|
||||
} else {
|
||||
const keysByAppleId = keys.filter(key => key.keyIdentifier === apiKeyId);
|
||||
if (keysByAppleId.length > 1) {
|
||||
throw new EasCommandError(
|
||||
`Multiple App Store Connect API keys match Apple key identifier "${apiKeyId}".`
|
||||
);
|
||||
} else if (keysByAppleId.length === 1) {
|
||||
apiKeyId = keysByAppleId[0].id;
|
||||
} else {
|
||||
throw new EasCommandError(
|
||||
`No App Store Connect API key found with Apple key identifier "${apiKeyId}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!apiKeyId) {
|
||||
throw new EasCommandError('No App Store Connect API key selected.');
|
||||
}
|
||||
|
||||
// Step 3: Discover remote apps
|
||||
const discoverSpinner = ora('Discovering App Store Connect apps').start();
|
||||
let remoteApps: DiscoveredAscApps;
|
||||
try {
|
||||
remoteApps = await AscAppLinkQuery.discoverAccessibleAppsAsync(
|
||||
graphqlClient,
|
||||
apiKeyId,
|
||||
flags['bundle-id']
|
||||
);
|
||||
discoverSpinner.succeed(`Found ${remoteApps.length} app(s) on App Store Connect`);
|
||||
} catch (err) {
|
||||
discoverSpinner.fail('Failed to discover apps');
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (remoteApps.length === 0) {
|
||||
throw new EasCommandError(
|
||||
'No accessible apps found on App Store Connect for the selected API key.' +
|
||||
(flags['bundle-id']
|
||||
? ` Try removing the --bundle-id filter or verify the bundle ID "${flags['bundle-id']}".`
|
||||
: '')
|
||||
);
|
||||
}
|
||||
|
||||
// Step 4: Select remote app
|
||||
let selectedApp: DiscoveredAscApps[number];
|
||||
if (flags['asc-app-id']) {
|
||||
const match = remoteApps.find(app => app.ascAppIdentifier === flags['asc-app-id']);
|
||||
if (!match) {
|
||||
throw new EasCommandError(
|
||||
`App with identifier "${flags['asc-app-id']}" was not found among accessible apps. Run ${chalk.bold('eas integrations:asc:connect')} interactively to discover available apps.`
|
||||
);
|
||||
}
|
||||
selectedApp = match;
|
||||
} else {
|
||||
selectedApp = await selectAsync<DiscoveredAscApps[number]>(
|
||||
'Select an App Store Connect app:',
|
||||
remoteApps.map(app => ({
|
||||
title: `${app.name} (${app.bundleIdentifier}) [${app.ascAppIdentifier}]`,
|
||||
value: app,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Step 5: Create link
|
||||
const createSpinner = ora('Connecting project to App Store Connect app').start();
|
||||
try {
|
||||
await AscAppLinkMutation.createAppStoreConnectAppAsync(graphqlClient, {
|
||||
appId: metadata.id,
|
||||
ascAppIdentifier: selectedApp.ascAppIdentifier,
|
||||
appStoreConnectApiKeyId: apiKeyId,
|
||||
});
|
||||
createSpinner.succeed('Connected project to App Store Connect app');
|
||||
} catch (err) {
|
||||
createSpinner.fail('Failed to connect project');
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Step 6: Refetch and display
|
||||
const refetchSpinner = ora('Verifying connection').start();
|
||||
const updatedMetadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
refetchSpinner.succeed('Verified connection');
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('connect', updatedMetadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(updatedMetadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import {
|
||||
buildInvalidJsonOutput,
|
||||
buildJsonOutput,
|
||||
formatAscAppLinkStatus,
|
||||
isAscAuthenticationError,
|
||||
} from '../../../integrations/asc/utils';
|
||||
import { AscAppLinkMutation } from '../../../graphql/mutations/AscAppLinkMutation';
|
||||
import { AscAppLinkQuery } from '../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { toggleConfirmAsync } from '../../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
|
||||
export default class IntegrationsAscDisconnect extends EasCommand {
|
||||
static override description = 'disconnect the current project from its App Store Connect app';
|
||||
|
||||
static override flags = {
|
||||
yes: Flags.boolean({
|
||||
description: 'Skip confirmation prompt',
|
||||
default: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(IntegrationsAscDisconnect);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(IntegrationsAscDisconnect, {
|
||||
nonInteractive: nonInteractive || flags.yes,
|
||||
});
|
||||
|
||||
// Step 1: Check current status
|
||||
const metadata = await this.fetchCurrentStatusAsync(graphqlClient, projectId);
|
||||
if (!metadata) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildInvalidJsonOutput('disconnect', projectId));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
'The App Store Connect API key linked to this project has been revoked or is no longer valid.\nThe connection cannot be resolved from the CLI. Please update the API key on the Expo dashboard.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!metadata.appStoreConnectApp) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('disconnect', metadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`Project ${chalk.bold(metadata.fullName)} is not connected to any App Store Connect app.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Confirm
|
||||
if (!flags.yes && !nonInteractive) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(metadata));
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
'You are about to disconnect this project from its App Store Connect app.\nThis action is reversible by reconnecting.'
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
if (!confirmed) {
|
||||
Log.error('Canceled disconnection');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Delete
|
||||
const deleteSpinner = ora('Disconnecting App Store Connect app').start();
|
||||
try {
|
||||
await AscAppLinkMutation.deleteAppStoreConnectAppAsync(
|
||||
graphqlClient,
|
||||
metadata.appStoreConnectApp.id
|
||||
);
|
||||
deleteSpinner.succeed('Disconnected App Store Connect app');
|
||||
} catch (err) {
|
||||
deleteSpinner.fail('Failed to disconnect App Store Connect app');
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Step 4: Refetch and display
|
||||
const refetchSpinner = ora('Verifying disconnection').start();
|
||||
const updatedMetadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId, {
|
||||
useCache: false,
|
||||
});
|
||||
refetchSpinner.succeed('Verified disconnection');
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('disconnect', updatedMetadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(updatedMetadata));
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchCurrentStatusAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string
|
||||
): Promise<Awaited<ReturnType<typeof AscAppLinkQuery.getAppMetadataAsync>> | null> {
|
||||
const spinner = ora('Checking current App Store Connect app link status').start();
|
||||
try {
|
||||
const metadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
spinner.succeed('Checked current status');
|
||||
return metadata;
|
||||
} catch (err) {
|
||||
if (isAscAuthenticationError(err)) {
|
||||
spinner.fail('App Store Connect connection is invalid');
|
||||
return null;
|
||||
}
|
||||
spinner.fail('Failed to check current status');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import EasCommand from '../../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../../commandUtils/flags';
|
||||
import {
|
||||
buildInvalidJsonOutput,
|
||||
buildJsonOutput,
|
||||
formatAscAppLinkStatus,
|
||||
isAscAuthenticationError,
|
||||
} from '../../../integrations/asc/utils';
|
||||
import { AscAppLinkQuery } from '../../../graphql/queries/AscAppLinkQuery';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
|
||||
export default class IntegrationsAscStatus extends EasCommand {
|
||||
static override description =
|
||||
'show the App Store Connect app link status for the current project';
|
||||
|
||||
static override flags = {
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(IntegrationsAscStatus);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(IntegrationsAscStatus, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const metadata = await this.fetchStatusAsync(graphqlClient, projectId);
|
||||
if (!metadata) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildInvalidJsonOutput('status', projectId));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
'The App Store Connect API key linked to this project has been revoked or is no longer valid.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildJsonOutput('status', metadata));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatAscAppLinkStatus(metadata));
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchStatusAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string
|
||||
): Promise<Awaited<ReturnType<typeof AscAppLinkQuery.getAppMetadataAsync>> | null> {
|
||||
const spinner = ora('Fetching App Store Connect app link status').start();
|
||||
try {
|
||||
const metadata = await AscAppLinkQuery.getAppMetadataAsync(graphqlClient, projectId);
|
||||
spinner.succeed('Fetched App Store Connect app link status');
|
||||
return metadata;
|
||||
} catch (err) {
|
||||
if (isAscAuthenticationError(err)) {
|
||||
spinner.fail('App Store Connect connection is invalid');
|
||||
return null;
|
||||
}
|
||||
spinner.fail('Failed to fetch App Store Connect app link status');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,10 @@ export default class SimulatorStart extends EasCommand {
|
||||
Log.log(`🔑 Run the following in your shell to attach to ${flags.type}:`);
|
||||
Log.newLine();
|
||||
Log.log(result.message);
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`When you are done, stop the session with: eas simulator:stop --id ${deviceRunSessionId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { DeviceRunSessionMutation } from '../../graphql/mutations/DeviceRunSessionMutation';
|
||||
import { ora } from '../../ora';
|
||||
|
||||
export default class SimulatorStop extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] stop a remote simulator session on EAS by its device run session ID';
|
||||
|
||||
static override flags = {
|
||||
id: Flags.string({
|
||||
description: 'Device run session ID',
|
||||
required: true,
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(SimulatorStop);
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(SimulatorStop, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
|
||||
const stopSpinner = ora(`🛑 Stopping device run session ${flags.id}`).start();
|
||||
try {
|
||||
const session = await DeviceRunSessionMutation.stopDeviceRunSessionAsync(
|
||||
graphqlClient,
|
||||
flags.id
|
||||
);
|
||||
stopSpinner.succeed(`🎉 Device run session ${session.id} is ${session.status.toLowerCase()}`);
|
||||
} catch (err) {
|
||||
stopSpinner.fail(`Failed to stop device run session ${flags.id}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
|
||||
import { formatAppleTeam } from './AppleTeamFormatting';
|
||||
import { AccountFragment, AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { confirmAsync, promptAsync } from '../../../prompts';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../../prompts';
|
||||
import { fromNow } from '../../../utils/date';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey
|
||||
|
||||
export enum AppStoreApiKeyPurpose {
|
||||
SUBMISSION_SERVICE = 'EAS Submit',
|
||||
ASC_APP_CONNECTION = 'EAS Connect',
|
||||
}
|
||||
|
||||
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
|
||||
@@ -99,12 +101,27 @@ async function generateAscApiKeyAsync(
|
||||
purpose: AppStoreApiKeyPurpose
|
||||
): Promise<MinimalAscApiKey> {
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const role = await selectRoleForGeneratedAscApiKeyAsync();
|
||||
const ascApiKey = await ctx.appStore.createAscApiKeyAsync(ctx.analytics, {
|
||||
nickname: getAscApiKeyName(purpose),
|
||||
roles: [role],
|
||||
});
|
||||
return await getMinimalAscApiKeyAsync(ascApiKey);
|
||||
}
|
||||
|
||||
async function selectRoleForGeneratedAscApiKeyAsync(): Promise<UserRole> {
|
||||
return await selectAsync<UserRole>('Select role for the generated API key:', [
|
||||
{
|
||||
title: 'ADMIN (default)',
|
||||
value: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
title: 'APP_MANAGER (least privilege for app management)',
|
||||
value: UserRole.APP_MANAGER,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
|
||||
const nameParts = ['[Expo]', purpose, nanoid(10)];
|
||||
return nameParts.join(' ');
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { promptAsync } from '../../../../prompts';
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { promptAsync, selectAsync } from '../../../../prompts';
|
||||
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
|
||||
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
|
||||
import {
|
||||
getCredentialsFromUserAsync,
|
||||
shouldAutoGenerateCredentialsAsync,
|
||||
} from '../../../utils/promptForCredentials';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
getAscApiKeyName,
|
||||
promptForAscApiKeyPathAsync,
|
||||
provideOrGenerateAscApiKeyAsync,
|
||||
} from '../AscApiKeyUtils';
|
||||
|
||||
jest.mock('../../../../prompts');
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
|
||||
return Object.keys(obj) as K[];
|
||||
@@ -18,7 +26,10 @@ function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
|
||||
|
||||
afterEach(() => {
|
||||
jest.mocked(promptAsync).mockClear();
|
||||
jest.mocked(selectAsync).mockClear();
|
||||
jest.mocked(getCredentialsFromUserAsync).mockClear();
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockClear();
|
||||
jest.mocked(fs.readFile).mockClear();
|
||||
});
|
||||
|
||||
describe(getAscApiKeyName, () => {
|
||||
@@ -87,3 +98,84 @@ describe(promptForAscApiKeyPathAsync, () => {
|
||||
expect(jest.mocked(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
|
||||
});
|
||||
});
|
||||
|
||||
describe(provideOrGenerateAscApiKeyAsync, () => {
|
||||
it('creates a key with ADMIN role by default', async () => {
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockResolvedValue(true);
|
||||
jest.mocked(selectAsync).mockResolvedValue(UserRole.ADMIN);
|
||||
|
||||
const createAscApiKeyAsync = jest.fn(async () => testAscApiKey);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
authCtx: testAuthCtx,
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
createAscApiKeyAsync,
|
||||
},
|
||||
});
|
||||
|
||||
await provideOrGenerateAscApiKeyAsync(ctx, AppStoreApiKeyPurpose.ASC_APP_CONNECTION);
|
||||
|
||||
expect(createAscApiKeyAsync).toHaveBeenCalledWith(ctx.analytics, {
|
||||
nickname: expect.any(String),
|
||||
roles: [UserRole.ADMIN],
|
||||
});
|
||||
});
|
||||
|
||||
it('allows creating a key with APP_MANAGER role', async () => {
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockResolvedValue(true);
|
||||
jest.mocked(selectAsync).mockResolvedValue(UserRole.APP_MANAGER);
|
||||
|
||||
const createAscApiKeyAsync = jest.fn(async () => testAscApiKey);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
authCtx: testAuthCtx,
|
||||
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
|
||||
createAscApiKeyAsync,
|
||||
},
|
||||
});
|
||||
|
||||
await provideOrGenerateAscApiKeyAsync(ctx, AppStoreApiKeyPurpose.ASC_APP_CONNECTION);
|
||||
|
||||
expect(createAscApiKeyAsync).toHaveBeenCalledWith(ctx.analytics, {
|
||||
nickname: expect.any(String),
|
||||
roles: [UserRole.APP_MANAGER],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not apply role override when using a user-provided key', async () => {
|
||||
jest.mocked(shouldAutoGenerateCredentialsAsync).mockResolvedValue(false);
|
||||
jest.mocked(promptAsync).mockResolvedValue({ keyP8Path: '/asc-api-key.p8' });
|
||||
jest.mocked(fs.readFile).mockImplementation(async () => 'test-key-p8' as any);
|
||||
jest
|
||||
.mocked(getCredentialsFromUserAsync)
|
||||
.mockResolvedValueOnce({ keyId: 'test-key-id' })
|
||||
.mockResolvedValueOnce({ issuerId: 'test-issuer-id' });
|
||||
|
||||
const createAscApiKeyAsync = jest.fn(async () => testAscApiKey);
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
appStore: {
|
||||
...getAppstoreMock(),
|
||||
authCtx: null,
|
||||
createAscApiKeyAsync,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await provideOrGenerateAscApiKeyAsync(
|
||||
ctx,
|
||||
AppStoreApiKeyPurpose.ASC_APP_CONNECTION
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
keyP8: expect.any(String),
|
||||
keyId: 'test-key-id',
|
||||
issuerId: 'test-issuer-id',
|
||||
});
|
||||
expect(selectAsync).not.toHaveBeenCalled();
|
||||
expect(createAscApiKeyAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ProfileType } from '@expo/app-store';
|
||||
import { UserRole } from '@expo/apple-utils';
|
||||
|
||||
import {
|
||||
AscApiKey,
|
||||
@@ -195,10 +196,10 @@ export default class AppStoreApi {
|
||||
|
||||
public async createAscApiKeyAsync(
|
||||
analytics: Analytics,
|
||||
{ nickname }: { nickname: string }
|
||||
{ nickname, roles }: { nickname: string; roles?: UserRole[] }
|
||||
): Promise<AscApiKey> {
|
||||
const userCtx = await this.ensureUserAuthenticatedAsync();
|
||||
return await createAscApiKeyAsync(analytics, userCtx, { nickname });
|
||||
return await createAscApiKeyAsync(analytics, userCtx, { nickname, roles });
|
||||
}
|
||||
|
||||
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
|
||||
|
||||
@@ -90,6 +90,23 @@ test(`createAscApiKeyAsync`, async () => {
|
||||
expect(result).toEqual({ ...mockAscApiKeyInfo, keyP8: 'super secret' });
|
||||
});
|
||||
|
||||
test(`createAscApiKeyAsync forwards explicit roles`, async () => {
|
||||
const analytics = instance(mock<Analytics>());
|
||||
const mockRequestContext = {};
|
||||
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
|
||||
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
|
||||
await createAscApiKeyAsync(analytics, mockAuthCtx, {
|
||||
nickname: 'test-name',
|
||||
roles: [UserRole.APP_MANAGER],
|
||||
});
|
||||
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
|
||||
nickname: 'test-name',
|
||||
allAppsVisible: true,
|
||||
roles: [UserRole.APP_MANAGER],
|
||||
keyType: ApiKeyType.PUBLIC_API,
|
||||
});
|
||||
});
|
||||
|
||||
test(`revokeAscApiKeyAsync`, async () => {
|
||||
const mockRequestContext = {};
|
||||
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
|
||||
|
||||
+179
@@ -1399,6 +1399,7 @@ export type App = Project & {
|
||||
*/
|
||||
buildsReleaseChannels: Array<Scalars['String']['output']>;
|
||||
channelsPaginated: AppChannelsConnection;
|
||||
convexProject?: Maybe<ConvexProject>;
|
||||
deployment?: Maybe<Deployment>;
|
||||
/** Deployments associated with this app */
|
||||
deployments: DeploymentsConnection;
|
||||
@@ -2086,6 +2087,9 @@ export type AppNotificationPreferenceInput = {
|
||||
export type AppObserve = {
|
||||
__typename?: 'AppObserve';
|
||||
appVersions: Array<AppObserveAppVersion>;
|
||||
customEventCounts: AppObserveCustomEventCounts;
|
||||
customEventList: AppObserveCustomEventListConnection;
|
||||
customEventNames: AppObserveCustomEventNames;
|
||||
environments: Array<Scalars['String']['output']>;
|
||||
events: AppObserveEventsConnection;
|
||||
timeSeries: AppObserveTimeSeries;
|
||||
@@ -2098,6 +2102,28 @@ export type AppObserveAppVersionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveCustomEventCountsArgs = {
|
||||
input: AppObserveCustomEventCountsInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveCustomEventListArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<AppObserveCustomEventListFilter>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveCustomEventNamesArgs = {
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveEnvironmentsArgs = {
|
||||
endTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
@@ -2163,6 +2189,96 @@ export type AppObserveAppVersionMetric = {
|
||||
statistics: AppObserveVersionMarkerStatistics;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEvent = {
|
||||
__typename?: 'AppObserveCustomEvent';
|
||||
appBuildNumber: Scalars['String']['output'];
|
||||
appEasBuildId?: Maybe<Scalars['String']['output']>;
|
||||
appUpdateId?: Maybe<Scalars['String']['output']>;
|
||||
appVersion: Scalars['String']['output'];
|
||||
countryCode?: Maybe<Scalars['String']['output']>;
|
||||
deviceModel: Scalars['String']['output'];
|
||||
deviceOs: Scalars['String']['output'];
|
||||
deviceOsVersion: Scalars['String']['output'];
|
||||
easClientId: Scalars['String']['output'];
|
||||
environment?: Maybe<Scalars['String']['output']>;
|
||||
eventName: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
properties: Array<AppObserveEventProperty>;
|
||||
sessionId?: Maybe<Scalars['String']['output']>;
|
||||
severityNumber?: Maybe<Scalars['Int']['output']>;
|
||||
severityText?: Maybe<Scalars['String']['output']>;
|
||||
timestamp: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventCountBucket = {
|
||||
__typename?: 'AppObserveCustomEventCountBucket';
|
||||
bucket: Scalars['DateTime']['output'];
|
||||
count: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventCounts = {
|
||||
__typename?: 'AppObserveCustomEventCounts';
|
||||
buckets: Array<AppObserveCustomEventCountBucket>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventCountsInput = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
bucketIntervalMinutes?: InputMaybe<Scalars['Int']['input']>;
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
eventName: Scalars['String']['input'];
|
||||
platform: AppObservePlatform;
|
||||
startTime: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventEdge = {
|
||||
__typename?: 'AppObserveCustomEventEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
node: AppObserveCustomEvent;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventListConnection = {
|
||||
__typename?: 'AppObserveCustomEventListConnection';
|
||||
edges: Array<AppObserveCustomEventEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventListFilter = {
|
||||
appBuildNumber?: InputMaybe<Scalars['String']['input']>;
|
||||
appEasBuildId?: InputMaybe<Scalars['String']['input']>;
|
||||
appUpdateId?: InputMaybe<Scalars['String']['input']>;
|
||||
appVersion?: InputMaybe<Scalars['String']['input']>;
|
||||
easClientId?: InputMaybe<Scalars['String']['input']>;
|
||||
endTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
eventName?: InputMaybe<Scalars['String']['input']>;
|
||||
platform?: InputMaybe<AppObservePlatform>;
|
||||
propertyFilters?: InputMaybe<Array<AppObserveCustomEventPropertyFilter>>;
|
||||
sessionId?: InputMaybe<Scalars['String']['input']>;
|
||||
startTime?: InputMaybe<Scalars['DateTime']['input']>;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventName = {
|
||||
__typename?: 'AppObserveCustomEventName';
|
||||
count: Scalars['Int']['output'];
|
||||
eventName: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventNames = {
|
||||
__typename?: 'AppObserveCustomEventNames';
|
||||
isTruncated: Scalars['Boolean']['output'];
|
||||
names: Array<AppObserveCustomEventName>;
|
||||
};
|
||||
|
||||
export type AppObserveCustomEventPropertyFilter = {
|
||||
key: Scalars['String']['input'];
|
||||
value: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type AppObserveEvent = {
|
||||
__typename?: 'AppObserveEvent';
|
||||
appBuildNumber: Scalars['String']['output'];
|
||||
@@ -2202,6 +2318,13 @@ export type AppObserveEventEdge = {
|
||||
node: AppObserveEvent;
|
||||
};
|
||||
|
||||
export type AppObserveEventProperty = {
|
||||
__typename?: 'AppObserveEventProperty';
|
||||
key: Scalars['String']['output'];
|
||||
type: AppObservePropertyType;
|
||||
value: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AppObserveEventsConnection = {
|
||||
__typename?: 'AppObserveEventsConnection';
|
||||
edges: Array<AppObserveEventEdge>;
|
||||
@@ -2242,6 +2365,13 @@ export enum AppObservePlatform {
|
||||
Ios = 'IOS'
|
||||
}
|
||||
|
||||
export enum AppObservePropertyType {
|
||||
Boolean = 'BOOLEAN',
|
||||
Json = 'JSON',
|
||||
Number = 'NUMBER',
|
||||
String = 'STRING'
|
||||
}
|
||||
|
||||
export type AppObserveReleasesInput = {
|
||||
endTime: Scalars['DateTime']['input'];
|
||||
environment?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -4089,6 +4219,18 @@ export type ConvexIntegrationQuery = {
|
||||
clientIdentifier: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ConvexProject = {
|
||||
__typename?: 'ConvexProject';
|
||||
app: App;
|
||||
convexProjectIdentifier: Scalars['String']['output'];
|
||||
convexProjectName: Scalars['String']['output'];
|
||||
convexProjectSlug: Scalars['String']['output'];
|
||||
convexTeamConnection: ConvexTeamConnection;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type ConvexTeamConnection = {
|
||||
__typename?: 'ConvexTeamConnection';
|
||||
account: Account;
|
||||
@@ -5610,6 +5752,7 @@ export enum EntityTypeName {
|
||||
BillingContractEntity = 'BillingContractEntity',
|
||||
BranchEntity = 'BranchEntity',
|
||||
ChannelEntity = 'ChannelEntity',
|
||||
ConvexProjectEntity = 'ConvexProjectEntity',
|
||||
ConvexTeamConnectionEntity = 'ConvexTeamConnectionEntity',
|
||||
CustomerEntity = 'CustomerEntity',
|
||||
EchoProjectEntity = 'EchoProjectEntity',
|
||||
@@ -11711,6 +11854,20 @@ export type CreateAppVersionMutationVariables = Exact<{
|
||||
|
||||
export type CreateAppVersionMutation = { __typename?: 'RootMutation', appVersion: { __typename?: 'AppVersionMutation', createAppVersion: { __typename?: 'AppVersion', id: string } } };
|
||||
|
||||
export type CreateAppStoreConnectAppMutationVariables = Exact<{
|
||||
appStoreConnectAppInput: AppStoreConnectAppInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateAppStoreConnectAppMutation = { __typename?: 'RootMutation', appStoreConnectApp: { __typename?: 'AppStoreConnectAppMutation', createAppStoreConnectApp: { __typename?: 'AppStoreConnectApp', id: string, ascAppIdentifier: string } } };
|
||||
|
||||
export type DeleteAppStoreConnectAppMutationVariables = Exact<{
|
||||
appStoreConnectAppId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteAppStoreConnectAppMutation = { __typename?: 'RootMutation', appStoreConnectApp: { __typename?: 'AppStoreConnectAppMutation', deleteAppStoreConnectApp: { __typename?: 'DeleteAppStoreConnectAppResult', id: string } } };
|
||||
|
||||
export type CreateAndroidBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
job: AndroidJobInput;
|
||||
@@ -11754,6 +11911,13 @@ export type CreateDeviceRunSessionMutationVariables = Exact<{
|
||||
|
||||
export type CreateDeviceRunSessionMutation = { __typename?: 'RootMutation', deviceRunSession: { __typename?: 'DeviceRunSessionMutation', createDeviceRunSession: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus, app: { __typename?: 'App', id: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, turtleJobRun?: { __typename?: 'JobRun', id: string } | null } } };
|
||||
|
||||
export type StopDeviceRunSessionMutationVariables = Exact<{
|
||||
deviceRunSessionId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type StopDeviceRunSessionMutation = { __typename?: 'RootMutation', deviceRunSession: { __typename?: 'DeviceRunSessionMutation', stopDeviceRunSession: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus } } };
|
||||
|
||||
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
accountId: Scalars['String']['input'];
|
||||
@@ -12063,6 +12227,21 @@ export type LatestAppVersionQueryVariables = Exact<{
|
||||
|
||||
export type LatestAppVersionQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, latestAppVersionByPlatformAndApplicationIdentifier?: { __typename?: 'AppVersion', id: string, storeVersion: string, buildVersion: string } | null } } };
|
||||
|
||||
export type AscAppLinkAppMetadataQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AscAppLinkAppMetadataQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, fullName: string, ownerAccount: { __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string } | { __typename?: 'User', id: string, username: string } | null, users: Array<{ __typename?: 'UserPermission', role: Role, actor: { __typename?: 'PartnerActor', id: string } | { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> }, appStoreConnectApp?: { __typename?: 'AppStoreConnectApp', id: string, ascAppIdentifier: string, remoteAppStoreConnectApp: { __typename?: 'RemoteAppStoreConnectApp', ascAppIdentifier: string, bundleIdentifier: string, name?: string | null, appStoreIconUrl?: string | null } } | null } } };
|
||||
|
||||
export type DiscoverAccessibleAppStoreConnectAppsQueryVariables = Exact<{
|
||||
appStoreConnectApiKeyId: Scalars['ID']['input'];
|
||||
bundleIdentifier?: InputMaybe<Scalars['String']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type DiscoverAccessibleAppStoreConnectAppsQuery = { __typename?: 'RootQuery', appStoreConnectApiKey: { __typename?: 'AppStoreConnectApiKeyQuery', byId: { __typename?: 'AppStoreConnectApiKey', id: string, remoteAppStoreConnectApps: Array<{ __typename?: 'RemoteAppStoreConnectApp', ascAppIdentifier: string, bundleIdentifier: string, name?: string | null, appStoreIconUrl?: string | null }> } } };
|
||||
|
||||
export type GetAssetSignedUrlsQueryVariables = Exact<{
|
||||
updateId: Scalars['ID']['input'];
|
||||
storageKeys: Array<Scalars['String']['input']> | Scalars['String']['input'];
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
|
||||
export const AscAppLinkMutation = {
|
||||
async createAppStoreConnectAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appStoreConnectAppInput: {
|
||||
appId: string;
|
||||
ascAppIdentifier: string;
|
||||
appStoreConnectApiKeyId: string;
|
||||
}
|
||||
): Promise<{ id: string; ascAppIdentifier: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appStoreConnectApp: {
|
||||
createAppStoreConnectApp: {
|
||||
id: string;
|
||||
ascAppIdentifier: string;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
gql`
|
||||
mutation CreateAppStoreConnectApp($appStoreConnectAppInput: AppStoreConnectAppInput!) {
|
||||
appStoreConnectApp {
|
||||
createAppStoreConnectApp(appStoreConnectAppInput: $appStoreConnectAppInput) {
|
||||
id
|
||||
ascAppIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appStoreConnectAppInput }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.appStoreConnectApp.createAppStoreConnectApp;
|
||||
},
|
||||
|
||||
async deleteAppStoreConnectAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appStoreConnectAppId: string
|
||||
): Promise<void> {
|
||||
await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{
|
||||
appStoreConnectApp: {
|
||||
deleteAppStoreConnectApp: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
gql`
|
||||
mutation DeleteAppStoreConnectApp($appStoreConnectAppId: ID!) {
|
||||
appStoreConnectApp {
|
||||
deleteAppStoreConnectApp(appStoreConnectAppId: $appStoreConnectAppId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appStoreConnectAppId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
CreateDeviceRunSessionInput,
|
||||
CreateDeviceRunSessionMutation,
|
||||
CreateDeviceRunSessionMutationVariables,
|
||||
StopDeviceRunSessionMutation,
|
||||
StopDeviceRunSessionMutationVariables,
|
||||
} from '../generated';
|
||||
|
||||
export const DeviceRunSessionMutation = {
|
||||
@@ -44,4 +46,28 @@ export const DeviceRunSessionMutation = {
|
||||
);
|
||||
return data.deviceRunSession.createDeviceRunSession;
|
||||
},
|
||||
async stopDeviceRunSessionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
deviceRunSessionId: string
|
||||
): Promise<StopDeviceRunSessionMutation['deviceRunSession']['stopDeviceRunSession']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<StopDeviceRunSessionMutation, StopDeviceRunSessionMutationVariables>(
|
||||
gql`
|
||||
mutation StopDeviceRunSessionMutation($deviceRunSessionId: ID!) {
|
||||
deviceRunSession {
|
||||
stopDeviceRunSession(deviceRunSessionId: $deviceRunSessionId) {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ deviceRunSessionId },
|
||||
{ noRetry: true }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.deviceRunSession.stopDeviceRunSession;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AscAppLinkAppMetadataQuery,
|
||||
AscAppLinkAppMetadataQueryVariables,
|
||||
DiscoverAccessibleAppStoreConnectAppsQuery,
|
||||
DiscoverAccessibleAppStoreConnectAppsQueryVariables,
|
||||
} from '../generated';
|
||||
import { AccountFragmentNode } from '../types/Account';
|
||||
|
||||
export const AscAppLinkQuery = {
|
||||
async getAppMetadataAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string,
|
||||
options?: {
|
||||
useCache?: boolean;
|
||||
}
|
||||
): Promise<AscAppLinkAppMetadataQuery['app']['byId']> {
|
||||
const useCache = options?.useCache ?? true;
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AscAppLinkAppMetadataQuery, AscAppLinkAppMetadataQueryVariables>(
|
||||
gql`
|
||||
query AscAppLinkAppMetadata($appId: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
fullName
|
||||
ownerAccount {
|
||||
id
|
||||
...AccountFragment
|
||||
}
|
||||
appStoreConnectApp {
|
||||
id
|
||||
ascAppIdentifier
|
||||
remoteAppStoreConnectApp {
|
||||
ascAppIdentifier
|
||||
bundleIdentifier
|
||||
name
|
||||
appStoreIconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(AccountFragmentNode)}
|
||||
`,
|
||||
{ appId },
|
||||
{
|
||||
requestPolicy: useCache ? 'cache-first' : 'network-only',
|
||||
additionalTypenames: ['App', 'AppStoreConnectApp'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.app.byId;
|
||||
},
|
||||
|
||||
async discoverAccessibleAppsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appStoreConnectApiKeyId: string,
|
||||
bundleIdentifier?: string
|
||||
): Promise<
|
||||
DiscoverAccessibleAppStoreConnectAppsQuery['appStoreConnectApiKey']['byId']['remoteAppStoreConnectApps']
|
||||
> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<
|
||||
DiscoverAccessibleAppStoreConnectAppsQuery,
|
||||
DiscoverAccessibleAppStoreConnectAppsQueryVariables
|
||||
>(
|
||||
gql`
|
||||
query DiscoverAccessibleAppStoreConnectApps(
|
||||
$appStoreConnectApiKeyId: ID!
|
||||
$bundleIdentifier: String
|
||||
) {
|
||||
appStoreConnectApiKey {
|
||||
byId(id: $appStoreConnectApiKeyId) {
|
||||
id
|
||||
remoteAppStoreConnectApps(bundleIdentifier: $bundleIdentifier) {
|
||||
ascAppIdentifier
|
||||
bundleIdentifier
|
||||
name
|
||||
appStoreIconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appStoreConnectApiKeyId, bundleIdentifier },
|
||||
{ additionalTypenames: ['AppStoreConnectApp'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.appStoreConnectApiKey.byId?.remoteAppStoreConnectApps ?? [];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import {
|
||||
buildInvalidJsonOutput,
|
||||
buildJsonOutput,
|
||||
formatAscAppLinkStatus,
|
||||
isAscAuthenticationError,
|
||||
} from '../utils';
|
||||
|
||||
const mockMetadataConnected = {
|
||||
id: 'app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: {
|
||||
id: 'asc-app-link-id',
|
||||
ascAppIdentifier: '1234567890',
|
||||
remoteAppStoreConnectApp: {
|
||||
ascAppIdentifier: '1234567890',
|
||||
bundleIdentifier: 'com.test.app',
|
||||
name: 'Test App',
|
||||
appStoreIconUrl: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockMetadataDisconnected = {
|
||||
id: 'app-id',
|
||||
fullName: '@testuser/testapp',
|
||||
ownerAccount: { id: 'account-id', name: 'testuser', ownerUserActor: null, users: [] },
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
|
||||
describe('buildJsonOutput', () => {
|
||||
it('returns connected output', () => {
|
||||
const output = buildJsonOutput('status', mockMetadataConnected);
|
||||
expect(output.action).toBe('status');
|
||||
expect(output.project).toBe('@testuser/testapp');
|
||||
expect(output.status).toBe('connected');
|
||||
expect(output.appStoreConnectApp).not.toBeNull();
|
||||
expect(output.appStoreConnectApp!.ascAppIdentifier).toBe('1234567890');
|
||||
expect(output.appStoreConnectApp!.name).toBe('Test App');
|
||||
expect(output.appStoreConnectApp!.bundleIdentifier).toBe('com.test.app');
|
||||
});
|
||||
|
||||
it('returns disconnected output', () => {
|
||||
const output = buildJsonOutput('status', mockMetadataDisconnected);
|
||||
expect(output.status).toBe('not-connected');
|
||||
expect(output.appStoreConnectApp).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInvalidJsonOutput', () => {
|
||||
it('returns invalid output', () => {
|
||||
const output = buildInvalidJsonOutput('status', 'project-id');
|
||||
expect(output.status).toBe('invalid');
|
||||
expect(output.project).toBe('project-id');
|
||||
expect(output.appStoreConnectApp).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAscAuthenticationError', () => {
|
||||
it('returns true for ASC authentication errors', () => {
|
||||
const error = new CombinedError({
|
||||
graphQLErrors: [
|
||||
new GraphQLError(
|
||||
'App Store Connect rejected this API key with status 401. Choose a valid API key and try again.'
|
||||
),
|
||||
],
|
||||
});
|
||||
expect(isAscAuthenticationError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for other CombinedErrors', () => {
|
||||
const error = new CombinedError({
|
||||
graphQLErrors: [new GraphQLError('Some other error')],
|
||||
});
|
||||
expect(isAscAuthenticationError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-CombinedError errors', () => {
|
||||
expect(isAscAuthenticationError(new Error('random error'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatAscAppLinkStatus', () => {
|
||||
it('formats connected status', () => {
|
||||
const status = formatAscAppLinkStatus(mockMetadataConnected);
|
||||
expect(status).toContain('Connected');
|
||||
expect(status).toContain('1234567890');
|
||||
expect(status).toContain('Test App');
|
||||
expect(status).toContain('com.test.app');
|
||||
});
|
||||
|
||||
it('formats disconnected status', () => {
|
||||
const status = formatAscAppLinkStatus(mockMetadataDisconnected);
|
||||
expect(status).toContain('Not connected');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
formatAscApiKey,
|
||||
provideOrGenerateAscApiKeyAsync,
|
||||
sortAscApiKeysByUpdatedAtDesc,
|
||||
} from '../../credentials/ios/actions/AscApiKeyUtils';
|
||||
import { AccountFragment, AppStoreConnectApiKeyFragment } from '../../graphql/generated';
|
||||
|
||||
export async function selectOrCreateAscApiKeyIdAsync({
|
||||
credentialsContext,
|
||||
existingKeys,
|
||||
ownerAccount,
|
||||
}: {
|
||||
credentialsContext: CredentialsContext;
|
||||
existingKeys: AppStoreConnectApiKeyFragment[];
|
||||
ownerAccount: AccountFragment;
|
||||
}): Promise<string> {
|
||||
const sortedKeys = sortAscApiKeysByUpdatedAtDesc(existingKeys);
|
||||
const createKeyOption = {
|
||||
title: '[Create or upload a new API key]',
|
||||
value: '__create_new_key__',
|
||||
};
|
||||
|
||||
const selectedValue =
|
||||
sortedKeys.length === 0
|
||||
? createKeyOption.value
|
||||
: await selectAsync<string>('Select an App Store Connect API key:', [
|
||||
...sortedKeys.map(key => ({
|
||||
title: formatAscApiKey(key),
|
||||
value: key.id,
|
||||
})),
|
||||
createKeyOption,
|
||||
]);
|
||||
|
||||
if (selectedValue !== createKeyOption.value) {
|
||||
return selectedValue;
|
||||
}
|
||||
|
||||
const newKey = await credentialsContext.ios.createAscApiKeyAsync(
|
||||
credentialsContext.graphqlClient,
|
||||
ownerAccount,
|
||||
await provideOrGenerateAscApiKeyAsync(
|
||||
credentialsContext,
|
||||
AppStoreApiKeyPurpose.ASC_APP_CONNECTION
|
||||
)
|
||||
);
|
||||
return newKey.id;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AscAppLinkQuery } from '../../graphql/queries/AscAppLinkQuery';
|
||||
|
||||
type AscAppLinkMetadata = Awaited<ReturnType<typeof AscAppLinkQuery.getAppMetadataAsync>>;
|
||||
|
||||
export type AscAppLinkStatus = 'not-connected' | 'connected' | 'invalid';
|
||||
|
||||
export interface AscAppLinkJsonOutput {
|
||||
action: string;
|
||||
project: string;
|
||||
status: AscAppLinkStatus;
|
||||
appStoreConnectApp: {
|
||||
id: string;
|
||||
ascAppIdentifier: string;
|
||||
name: string | null;
|
||||
bundleIdentifier: string | null;
|
||||
appleUrl: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function buildJsonOutput(
|
||||
action: string,
|
||||
metadata: AscAppLinkMetadata
|
||||
): AscAppLinkJsonOutput {
|
||||
const link = metadata.appStoreConnectApp;
|
||||
return {
|
||||
action,
|
||||
project: metadata.fullName,
|
||||
status: link ? 'connected' : 'not-connected',
|
||||
appStoreConnectApp: link
|
||||
? {
|
||||
id: link.id,
|
||||
ascAppIdentifier: link.ascAppIdentifier,
|
||||
name: link.remoteAppStoreConnectApp.name ?? null,
|
||||
bundleIdentifier: link.remoteAppStoreConnectApp.bundleIdentifier ?? null,
|
||||
appleUrl: getAppleAppUrl(link.ascAppIdentifier),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInvalidJsonOutput(action: string, projectId: string): AscAppLinkJsonOutput {
|
||||
return {
|
||||
action,
|
||||
project: projectId,
|
||||
status: 'invalid',
|
||||
appStoreConnectApp: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function isAscAuthenticationError(error: unknown): error is CombinedError {
|
||||
return (
|
||||
error instanceof CombinedError &&
|
||||
error.graphQLErrors.some(e => e.message.includes('App Store Connect rejected this API key'))
|
||||
);
|
||||
}
|
||||
|
||||
export function formatAscAppLinkStatus(metadata: AscAppLinkMetadata): string {
|
||||
const link = metadata.appStoreConnectApp;
|
||||
if (!link) {
|
||||
return `Project ${chalk.bold(metadata.fullName)}: ${chalk.yellow('Not connected')} to App Store Connect.`;
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
`Project ${chalk.bold(metadata.fullName)}: ${chalk.green('Connected')} to App Store Connect.`,
|
||||
` ASC App ID: ${chalk.bold(link.ascAppIdentifier)}`,
|
||||
];
|
||||
|
||||
const remote = link.remoteAppStoreConnectApp;
|
||||
lines.push(` Name: ${remote.name}`);
|
||||
lines.push(` Bundle ID: ${remote.bundleIdentifier}`);
|
||||
lines.push(` Apple URL: ${getAppleAppUrl(link.ascAppIdentifier)}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function getAppleAppUrl(ascAppIdentifier: string): string {
|
||||
return `https://appstoreconnect.apple.com/apps/${encodeURIComponent(ascAppIdentifier)}/distribution`;
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function wrapFetchWithProgress(): (
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadFileWithProgressTrackerAsync(
|
||||
export async function downloadFileWithProgressTrackerAsync(
|
||||
url: string,
|
||||
outputPath: string,
|
||||
progressTrackerMessage: string | ((ratio: number, total: number) => string),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "18.8.1",
|
||||
"version": "18.9.0",
|
||||
"description": "Tool for running EAS compatible builds on a local machine.",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
@@ -26,7 +26,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "18.8.1",
|
||||
"@expo/build-tools": "18.9.0",
|
||||
"@expo/eas-build-job": "18.8.0",
|
||||
"@expo/logger": "18.5.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/steps",
|
||||
"version": "18.8.0",
|
||||
"version": "18.9.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { JobInterpolationContext } from '@expo/eas-build-job';
|
||||
|
||||
import { interpolateJobContext } from '../interpolation';
|
||||
|
||||
describe(interpolateJobContext, () => {
|
||||
it('interpolates multiple ${{ }} segments when the string starts with ${{ and ends with }}', () => {
|
||||
const context = {
|
||||
github: {
|
||||
event: {
|
||||
pull_request: {
|
||||
html_url: 'https://github.com/acme/app/pull/42',
|
||||
title: 'Fix crash',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as JobInterpolationContext;
|
||||
|
||||
expect(
|
||||
interpolateJobContext({
|
||||
target: '${{ github.event.pull_request.html_url }}: ${{ github.event.pull_request.title }}',
|
||||
context,
|
||||
})
|
||||
).toBe('https://github.com/acme/app/pull/42: Fix crash');
|
||||
});
|
||||
|
||||
it('still preserves non-string types for a single whole-string interpolation', () => {
|
||||
const context = {
|
||||
build: { profile: 'production', id: '123' },
|
||||
} as unknown as JobInterpolationContext;
|
||||
|
||||
expect(
|
||||
interpolateJobContext({
|
||||
target: '${{ build }}',
|
||||
context,
|
||||
})
|
||||
).toEqual({ profile: 'production', id: '123' });
|
||||
});
|
||||
|
||||
it("handles a single interpolation when the expression contains '}}' in a string literal", () => {
|
||||
const context = {
|
||||
build: { profile: 'production}}' },
|
||||
} as unknown as JobInterpolationContext;
|
||||
|
||||
expect(
|
||||
interpolateJobContext({
|
||||
target: "${{ build.profile == 'production}}' }}",
|
||||
context,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,10 @@ import { JobInterpolationContext } from '@expo/eas-build-job';
|
||||
|
||||
import { jsepEval } from './utils/jsepEval';
|
||||
|
||||
function isSingleWholeStringInterpolation(value: string): boolean {
|
||||
return value.startsWith('${{') && value.endsWith('}}') && !value.includes('${{', 3);
|
||||
}
|
||||
|
||||
export function interpolateJobContext({
|
||||
target,
|
||||
context,
|
||||
@@ -12,7 +16,7 @@ export function interpolateJobContext({
|
||||
if (typeof target === 'string') {
|
||||
// If the value is e.g. `build: ${{ inputs.build }}`, we will interpolate the value
|
||||
// without changing `inputs.build` type, i.e. if it is an object it'll be like `build: {...inputs.build}`.
|
||||
if (target.startsWith('${{') && target.endsWith('}}')) {
|
||||
if (isSingleWholeStringInterpolation(target)) {
|
||||
return jsepEval(target.slice(3, -2), context);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,3 +14,4 @@ if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
|
||||
|
||||
// Always mock:
|
||||
jest.mock('../src/CacheManager');
|
||||
jest.mock('../src/utils/turtleFetch');
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "18.8.1",
|
||||
"version": "18.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "18.8.1",
|
||||
"@expo/build-tools": "18.9.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "18.5.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.8.0",
|
||||
"@expo/steps": "18.9.0",
|
||||
"@expo/template-file": "18.5.0",
|
||||
"@expo/turtle-spawn": "18.5.0",
|
||||
"@hapi/boom": "10.0.1",
|
||||
|
||||
@@ -70,6 +70,7 @@ jest.mock('../config', () => {
|
||||
default: {
|
||||
...config.default,
|
||||
buildId: 'e9b99e52-fb74-4927-be63-33d7447ddfd4',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ jest.mock('../config', () => {
|
||||
return {
|
||||
...config,
|
||||
buildId: 'e9b99e52-fb74-4927-be63-33d7447ddfd4',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ jest.mock('../config', () => {
|
||||
return {
|
||||
...config,
|
||||
buildId: 'f38532aa-81a8-4db7-915f-6e7afe46e22f',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ jest.mock('../config', () => {
|
||||
return {
|
||||
...config,
|
||||
buildId: 'e9b99e52-fb74-4927-be63-33d7447ddfd4',
|
||||
wwwApiV2BaseUrl: 'http://api.expo.test/v2/',
|
||||
};
|
||||
});
|
||||
jest.mock('../upload', () => {
|
||||
|
||||
@@ -5,9 +5,11 @@ import { getBuildEnv } from '../env';
|
||||
|
||||
describe(getBuildEnv.name, () => {
|
||||
const originalSentryDsn = config.sentry.dsn;
|
||||
const originalCocoapodsCacheUrl = config.cocoapodsCacheUrl;
|
||||
|
||||
afterEach(() => {
|
||||
config.sentry.dsn = originalSentryDsn;
|
||||
config.cocoapodsCacheUrl = originalCocoapodsCacheUrl;
|
||||
});
|
||||
|
||||
it('passes the CLI sentry DSN to worker child processes', () => {
|
||||
@@ -55,6 +57,36 @@ describe(getBuildEnv.name, () => {
|
||||
});
|
||||
|
||||
expect(env.EXPO_USE_PRECOMPILED_MODULES).toBe('1');
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_BASE_URL).toBe(
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules/'
|
||||
);
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_PATH).toBeUndefined();
|
||||
});
|
||||
|
||||
it('passes precompiled modules through the CocoaPods cache URL when available', () => {
|
||||
config.cocoapodsCacheUrl = 'https://cache.example.com';
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.IOS,
|
||||
type: Workflow.GENERIC,
|
||||
builderEnvironment: {
|
||||
env: {
|
||||
EAS_USE_PRECOMPILED_MODULES: '1',
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_BASE_URL).toBe(
|
||||
'https://cache.example.com/storage.googleapis.com/eas-build-precompiled-modules/'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
} from './external/turtle';
|
||||
import { getAccessedEnvs } from './utils/env';
|
||||
|
||||
const PRECOMPILED_MODULES_BASE_URL =
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules/';
|
||||
|
||||
// keep in sync with local-build-plugin env vars
|
||||
// see packages/local-build-plugin/src/build.ts
|
||||
export function getBuildEnv({
|
||||
@@ -55,6 +58,7 @@ export function getBuildEnv({
|
||||
setEnv(env, 'COMPILER_INDEX_STORE_ENABLE', 'NO');
|
||||
if (job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES === '1') {
|
||||
setEnv(env, 'EXPO_USE_PRECOMPILED_MODULES', '1');
|
||||
setEnv(env, 'EXPO_PRECOMPILED_MODULES_BASE_URL', getPrecompiledModulesBaseUrl());
|
||||
}
|
||||
|
||||
if (job.builderEnvironment?.env?.EAS_USE_CACHE === '1') {
|
||||
@@ -166,6 +170,18 @@ function setEnv(env: Env, key: string, value: string | null | undefined): void {
|
||||
}
|
||||
}
|
||||
|
||||
function getPrecompiledModulesBaseUrl(): string {
|
||||
if (!config.cocoapodsCacheUrl) {
|
||||
return PRECOMPILED_MODULES_BASE_URL;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(PRECOMPILED_MODULES_BASE_URL);
|
||||
return PRECOMPILED_MODULES_BASE_URL.replace(
|
||||
`${parsedUrl.protocol}//${parsedUrl.host}`,
|
||||
`${config.cocoapodsCacheUrl.replace(/\/$/, '')}/${parsedUrl.host}`
|
||||
);
|
||||
}
|
||||
|
||||
const ResourceClassToMaxHeapSize: Record<ResourceClass, string | null> = {
|
||||
[ResourceClass.LINUX_C3D_STANDARD_4]: '4g', // 4 out of 16 GB, 12 GB left
|
||||
[ResourceClass.LINUX_C3D_STANDARD_8]: '8g', // 8 out of 32 GB, 24 GB left
|
||||
|
||||
@@ -332,7 +332,7 @@ export default class BuildService {
|
||||
});
|
||||
|
||||
const robotAccessToken = job.secrets?.robotAccessToken;
|
||||
if (robotAccessToken && err.errorCode === errors.ErrorCode.UNKNOWN_ERROR) {
|
||||
if (robotAccessToken) {
|
||||
let rawErrorMessage: string = '';
|
||||
if (maybeRawError?.stderr) {
|
||||
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stderr);
|
||||
@@ -340,23 +340,42 @@ export default class BuildService {
|
||||
if (maybeRawError?.stdout) {
|
||||
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stdout);
|
||||
}
|
||||
if (!rawErrorMessage) {
|
||||
rawErrorMessage = maybeRawError?.message ?? err.message;
|
||||
}
|
||||
|
||||
await turtleFetch(
|
||||
new URL('turtle-builds/error-logs', config.wwwApiV2BaseUrl).toString(),
|
||||
'POST',
|
||||
{
|
||||
json: {
|
||||
buildId: this.buildId,
|
||||
message: rawErrorMessage,
|
||||
buildPhase: err.buildPhase ?? null,
|
||||
errorCode: err.errorCode,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
shouldThrowOnNotOk: false,
|
||||
}
|
||||
);
|
||||
try {
|
||||
await turtleFetch(
|
||||
new URL('turtle-builds/error-logs', config.wwwApiV2BaseUrl).toString(),
|
||||
'POST',
|
||||
{
|
||||
json: {
|
||||
buildId: this.buildId,
|
||||
message: rawErrorMessage,
|
||||
buildPhase: err.buildPhase ?? null,
|
||||
errorCode: err.errorCode,
|
||||
tags: {
|
||||
platform: job.platform,
|
||||
workflow: job.type,
|
||||
sdk_version: metadata?.sdkVersion ?? null,
|
||||
react_native_version: metadata?.reactNativeVersion ?? null,
|
||||
app_id: job.appId ?? null,
|
||||
build_profile: metadata?.buildProfile ?? null,
|
||||
app_name: metadata?.appName ?? null,
|
||||
app_identifier: metadata?.appIdentifier ?? null,
|
||||
distribution: metadata?.distribution ?? null,
|
||||
cli_version: metadata?.cliVersion ?? null,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
shouldThrowOnNotOk: false,
|
||||
}
|
||||
);
|
||||
} catch (fetchError: any) {
|
||||
logger.warn({ err: fetchError }, 'Failed to send build error log');
|
||||
}
|
||||
}
|
||||
|
||||
await this.finishError(err, maybeArtifacts);
|
||||
|
||||
@@ -2856,7 +2856,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:18.8.1, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:18.9.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
@@ -2871,7 +2871,7 @@ __metadata:
|
||||
"@expo/repack-app": "npm:~0.4.1"
|
||||
"@expo/results": "npm:^1.0.0"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@expo/xcpretty": "npm:^4.3.1"
|
||||
@@ -3481,7 +3481,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/steps@npm:18.8.0, @expo/steps@workspace:packages/steps":
|
||||
"@expo/steps@npm:18.9.0, @expo/steps@workspace:packages/steps":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/steps@workspace:packages/steps"
|
||||
dependencies:
|
||||
@@ -3554,7 +3554,7 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:18.8.1"
|
||||
"@expo/build-tools": "npm:18.9.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/downloader": "npm:18.5.0"
|
||||
@@ -3563,7 +3563,7 @@ __metadata:
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@expo/template-file": "npm:18.5.0"
|
||||
"@expo/turtle-spawn": "npm:18.5.0"
|
||||
"@hapi/boom": "npm:10.0.1"
|
||||
@@ -10331,7 +10331,7 @@ __metadata:
|
||||
resolution: "create-eas-build-function@workspace:packages/create-eas-build-function"
|
||||
dependencies:
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
@@ -10944,7 +10944,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:18.8.1"
|
||||
"@expo/build-tools": "npm:18.9.0"
|
||||
"@expo/eas-build-job": "npm:18.8.0"
|
||||
"@expo/logger": "npm:18.5.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
@@ -11018,7 +11018,7 @@ __metadata:
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:18.8.0"
|
||||
"@expo/steps": "npm:18.9.0"
|
||||
"@expo/timeago.js": "npm:1.0.0"
|
||||
"@graphql-codegen/cli": "npm:5.0.0"
|
||||
"@graphql-codegen/introspection": "npm:4.0.0"
|
||||
|
||||
Reference in new issue
Block a user