mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb6713f12 | ||
|
|
24a1d2d581 | ||
|
|
8af88c2d29 | ||
|
|
d416ac2162 | ||
|
|
7bf0493a4d | ||
|
|
91f32cc377 | ||
|
|
cf54f62bac | ||
|
|
3ef3e8447c | ||
|
|
ad64e1afad | ||
|
|
a0800cf7ef | ||
|
|
34436ee674 | ||
|
|
1072b75927 | ||
|
|
c0c9d2e7df | ||
|
|
19b831667f | ||
|
|
09db7da23b | ||
|
|
f08dde2469 | ||
|
|
213b5d240c | ||
|
|
16ebdce913 | ||
|
|
121ff7a718 | ||
|
|
360393f647 | ||
|
|
82d66054dc | ||
|
|
d8849dbf8c | ||
|
|
4d6b5f435b | ||
|
|
8fe7dfea29 | ||
|
|
8750155c25 | ||
|
|
a0fce1c773 | ||
|
|
cad3dca5df | ||
|
|
2ef6e79395 | ||
|
|
44270814a7 | ||
|
|
95155681db | ||
|
|
53fd599d0a | ||
|
|
b51201d010 | ||
|
|
f86c079733 |
No files matched your search
@@ -37,7 +37,7 @@ body:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
|
||||
description: Run `npx expo-env-info` and `npx expo-doctor`, and paste the output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
|
||||
+55
-2
@@ -8,12 +8,65 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [3.9.2](https://github.com/expo/eas-cli/releases/tag/v3.9.2) - 2023-04-13
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixes the rollout percentages when ending a rollout. ([#1781](https://github.com/expo/eas-cli/pull/1781) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Print meaningful error message if `extra.eas.projectId` is not a string. ([#1788](https://github.com/expo/eas-cli/pull/1788) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Show output of `expo install expo-updates`. ([#1782](https://github.com/expo/eas-cli/pull/1782) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Print deprecation warnings for deprecated `eas.json` fields. ([#1768](https://github.com/expo/eas-cli/pull/1768) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.9.1](https://github.com/expo/eas-cli/releases/tag/v3.9.1) - 2023-04-10
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Update apple utilities to work around the maintenance error. ([#1780](https://github.com/expo/eas-cli/pull/1780) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Don't use defaults for the `cache.cacheDefaultPaths` build profile field. ([#1769](https://github.com/expo/eas-cli/pull/1769) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.9.0](https://github.com/expo/eas-cli/releases/tag/v3.9.0) - 2023-04-06
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for roll back to embedded updates. ([#1754](https://github.com/expo/eas-cli/pull/1754), [#1755](https://github.com/expo/eas-cli/pull/1755) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Update `expoCommandAsync` to support the new Expo CLI bin path. ([#1772](https://github.com/expo/eas-cli/pull/1772) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Use the `eas-cli` npm tag for checking for the local build plugin updates. ([#1759](https://github.com/expo/eas-cli/pull/1759) by [@dsokal](https://github.com/dsokal))
|
||||
- Ignore `requireCommit` option in `eas build:internal`. ([#1760](https://github.com/expo/eas-cli/pull/1760) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support parsing eas.json from a string. ([#1766](https://github.com/expo/eas-cli/pull/1766) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [3.8.1](https://github.com/expo/eas-cli/releases/tag/v3.8.1) - 2023-03-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Ensure public config is used as update. ([#1745](https://github.com/expo/eas-cli/pull/1745) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Unify how the command errors are displayed. ([#1738](https://github.com/expo/eas-cli/pull/1738) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
- Unify the case of base error messages for command failures. ([#1744](https://github.com/expo/eas-cli/pull/1744) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
|
||||
## [3.8.0](https://github.com/expo/eas-cli/releases/tag/v3.8.0) - 2023-03-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.7.2](https://github.com/expo/eas-cli/releases/tag/v3.7.2) - 2023-02-24
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "3.8.0",
|
||||
"version": "3.9.3",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+79
-54
@@ -79,6 +79,7 @@ eas --help COMMAND
|
||||
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
|
||||
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
|
||||
* [`eas channel:list`](#eas-channellist)
|
||||
* [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel)
|
||||
* [`eas channel:view [NAME]`](#eas-channelview-name)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
@@ -130,7 +131,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -147,7 +148,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -164,7 +165,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -178,7 +179,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -228,7 +229,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -249,7 +250,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -269,7 +270,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -289,7 +290,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -312,7 +313,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -342,7 +343,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -359,7 +360,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -376,7 +377,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -411,7 +412,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -448,7 +449,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -474,7 +475,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -498,7 +499,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -545,7 +546,7 @@ DESCRIPTION
|
||||
Update version of an app.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -564,7 +565,7 @@ DESCRIPTION
|
||||
Update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -581,7 +582,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -602,7 +603,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -624,7 +625,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -644,7 +645,31 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
Roll a new branch out on a channel incrementally.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:rollout [CHANNEL] [--branch <value>] [--percent <value>] [--end] [--json --non-interactive]
|
||||
|
||||
ARGUMENTS
|
||||
CHANNEL channel on which the rollout should be done
|
||||
|
||||
FLAGS
|
||||
--branch=<value> branch to rollout
|
||||
--end end the rollout
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--percent=<value> percent of users to send to the new branch
|
||||
|
||||
DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -667,7 +692,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -688,7 +713,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -705,7 +730,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -719,7 +744,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -739,7 +764,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -760,7 +785,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -774,7 +799,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -788,7 +813,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -876,7 +901,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -893,7 +918,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -910,7 +935,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas open`
|
||||
|
||||
@@ -924,7 +949,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -938,7 +963,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -960,7 +985,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -983,7 +1008,7 @@ DESCRIPTION
|
||||
create an environment secret on the current project or owner account
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -1001,7 +1026,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -1015,7 +1040,7 @@ DESCRIPTION
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas secret:push`
|
||||
|
||||
@@ -1035,7 +1060,7 @@ DESCRIPTION
|
||||
read environment secrets from env file and store on the server
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1065,7 +1090,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1097,7 +1122,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1115,7 +1140,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1136,7 +1161,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1158,11 +1183,11 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
rollback to an existing update
|
||||
roll back to an existing update
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -1179,10 +1204,10 @@ FLAGS
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
rollback to an existing update
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -1202,7 +1227,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -1223,7 +1248,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -1243,7 +1268,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -1261,7 +1286,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -1283,7 +1308,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1300,7 +1325,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.8.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
|
||||
Generated
+1174
-156
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "3.8.0",
|
||||
"version": "3.9.3",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "1.1.0",
|
||||
"@expo/apple-utils": "1.2.0",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "7.0.3",
|
||||
"@expo/config-plugins": "5.0.4",
|
||||
"@expo/config-types": "47.0.0",
|
||||
"@expo/eas-build-job": "0.2.106",
|
||||
"@expo/eas-json": "3.8.0",
|
||||
"@expo/eas-build-job": "1.0.7",
|
||||
"@expo/eas-json": "3.9.2",
|
||||
"@expo/json-file": "8.2.37",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
@@ -27,6 +27,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/steps": "1.0.8",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.23.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.10",
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import { EasCommandError } from '../../commandUtils/errors';
|
||||
import Build from '../../commands/build';
|
||||
import Log, { link } from '../../log';
|
||||
import { handleBuildRequestError } from '../build';
|
||||
import {
|
||||
EasBuildDownForMaintenanceError,
|
||||
EasBuildFreeTierDisabledAndroidError,
|
||||
EasBuildFreeTierDisabledError,
|
||||
EasBuildFreeTierDisabledIOSError,
|
||||
EasBuildTooManyPendingBuildsError,
|
||||
RequestValidationError,
|
||||
TurtleDeprecatedJobFormatError,
|
||||
} from '../errors';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON = `[
|
||||
{
|
||||
"message": "Error 1"
|
||||
},
|
||||
{
|
||||
"message": "Error 2"
|
||||
},
|
||||
{
|
||||
"message": "Error 3"
|
||||
}
|
||||
]`;
|
||||
|
||||
const EXPECTED_EAS_BUILD_DOWN_MESSAGE = `EAS Build is down for maintenance. Try again later. Check ${link(
|
||||
'https://status.expo.dev/'
|
||||
)} for updates.`;
|
||||
|
||||
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID = `You have already reached the maximum number of pending Android builds for your account. Try again later.`;
|
||||
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS = `You have already reached the maximum number of pending iOS builds for your account. Try again later.`;
|
||||
|
||||
const EXPECTED_GENERIC_MESSAGE =
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.';
|
||||
|
||||
const getGraphQLError = (message: string, errorCode: string): GraphQLError => {
|
||||
return new GraphQLError(message, null, null, null, null, null, {
|
||||
errorCode,
|
||||
});
|
||||
};
|
||||
|
||||
const assertReThrownError = (
|
||||
caughtError: CombinedError | Error,
|
||||
expectedType: typeof EasCommandError,
|
||||
expectedMessage: string = 'Error 1'
|
||||
): void => {
|
||||
expect(caughtError).toBeInstanceOf(expectedType);
|
||||
expect((caughtError as Error).message).toEqual(expectedMessage);
|
||||
};
|
||||
|
||||
describe(Build.name, () => {
|
||||
describe('handle build request errors', () => {
|
||||
describe('logs graphQL errors to debug if present', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logs nothing to debug if graphQLErrors not present', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const error = new Error('Generic error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const error = new Error('Generic error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is server-side defined error', () => {
|
||||
describe('throws correct EasCommandError subclass', () => {
|
||||
describe('for TURTLE_DEPRECATED_JOB_FORMAT', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('for EAS_BUILD_FREE_TIER_DISABLED', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does it for EAS_BUILD_FREE_TIER_DISABLED_IOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_IOS');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledIOSError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it for EAS_BUILD_FREE_TIER_DISABLED_ANDROID', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledAndroidError);
|
||||
}
|
||||
});
|
||||
|
||||
describe('for VALIDATION_ERROR', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, RequestValidationError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, RequestValidationError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is other expected graphQL-related error', () => {
|
||||
describe('throws correct EasCommandError subclass', () => {
|
||||
describe('for EAS_BUILD_DOWN_FOR_MAINTENANCE', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildDownForMaintenanceError,
|
||||
EXPECTED_EAS_BUILD_DOWN_MESSAGE
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildDownForMaintenanceError,
|
||||
EXPECTED_EAS_BUILD_DOWN_MESSAGE
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('for EAS_BUILD_TOO_MANY_PENDING_BUILDS', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildTooManyPendingBuildsError,
|
||||
EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildTooManyPendingBuildsError,
|
||||
EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is unexpected graphQL-related error', () => {
|
||||
describe('throws base Error class with custom message', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is unexpected non-graphQL-related error', () => {
|
||||
describe('throws base Error class with propagated message', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const error = new Error('Non-graphQL-related error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
expect(caughtError).toStrictEqual(error);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const error = new Error('Non-graphQL-related error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
expect(caughtError).toStrictEqual(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,6 @@ interface JobData {
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasCommandError } from '../commandUtils/errors';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
@@ -36,6 +37,15 @@ import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import {
|
||||
EasBuildDownForMaintenanceError,
|
||||
EasBuildFreeTierDisabledAndroidError,
|
||||
EasBuildFreeTierDisabledError,
|
||||
EasBuildFreeTierDisabledIOSError,
|
||||
EasBuildTooManyPendingBuildsError,
|
||||
RequestValidationError,
|
||||
TurtleDeprecatedJobFormatError,
|
||||
} from './errors';
|
||||
import { transformMetadata } from './graphql';
|
||||
import { LocalBuildMode, runLocalBuildAsync } from './local';
|
||||
import { collectMetadataAsync } from './metadata';
|
||||
@@ -161,38 +171,33 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
};
|
||||
}
|
||||
|
||||
const SERVER_SIDE_DEFINED_ERRORS = [
|
||||
'TURTLE_DEPRECATED_JOB_FORMAT',
|
||||
'EAS_BUILD_FREE_TIER_DISABLED',
|
||||
'EAS_BUILD_FREE_TIER_DISABLED_IOS',
|
||||
'EAS_BUILD_FREE_TIER_DISABLED_ANDROID',
|
||||
'VALIDATION_ERROR',
|
||||
];
|
||||
const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
|
||||
TURTLE_DEPRECATED_JOB_FORMAT: TurtleDeprecatedJobFormatError,
|
||||
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError,
|
||||
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError,
|
||||
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError,
|
||||
VALIDATION_ERROR: RequestValidationError,
|
||||
};
|
||||
|
||||
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
export function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
Log.debug(JSON.stringify(error.graphQLErrors, null, 2));
|
||||
|
||||
if (SERVER_SIDE_DEFINED_ERRORS.includes(error?.graphQLErrors?.[0]?.extensions?.errorCode)) {
|
||||
Log.error(error?.graphQLErrors?.[0]?.message);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
const graphQLErrorCode: string = error?.graphQLErrors?.[0]?.extensions?.errorCode;
|
||||
if (graphQLErrorCode in SERVER_SIDE_DEFINED_ERRORS) {
|
||||
const ErrorClass: typeof EasCommandError = SERVER_SIDE_DEFINED_ERRORS[graphQLErrorCode];
|
||||
throw new ErrorClass(error?.graphQLErrors?.[0]?.message);
|
||||
} else if (graphQLErrorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE') {
|
||||
throw new EasBuildDownForMaintenanceError(
|
||||
`EAS Build is down for maintenance. Try again later. Check ${link(
|
||||
'https://status.expo.dev/'
|
||||
)} for updates.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
Log.error(
|
||||
} else if (graphQLErrorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS') {
|
||||
throw new EasBuildTooManyPendingBuildsError(
|
||||
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
throw new Error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.'
|
||||
);
|
||||
}
|
||||
@@ -607,7 +612,7 @@ function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
}
|
||||
|
||||
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
|
||||
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { EasCommandError } from '../commandUtils/errors';
|
||||
|
||||
export class TurtleDeprecatedJobFormatError extends EasCommandError {}
|
||||
|
||||
export class EasBuildFreeTierDisabledError extends EasCommandError {}
|
||||
|
||||
export class EasBuildFreeTierDisabledIOSError extends EasCommandError {}
|
||||
|
||||
export class EasBuildFreeTierDisabledAndroidError extends EasCommandError {}
|
||||
|
||||
export class RequestValidationError extends EasCommandError {}
|
||||
|
||||
export class EasBuildDownForMaintenanceError extends EasCommandError {}
|
||||
|
||||
export class EasBuildTooManyPendingBuildsError extends EasCommandError {}
|
||||
@@ -28,7 +28,6 @@ interface JobData {
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function runBuildAndSubmitAsync(
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Command } from '@oclif/core';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import {
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import Log from '../log';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import AnalyticsContextField from './context/AnalyticsContextField';
|
||||
import ContextField from './context/ContextField';
|
||||
@@ -16,6 +18,7 @@ import { OptionalProjectConfigContextField } from './context/OptionalProjectConf
|
||||
import ProjectConfigContextField from './context/ProjectConfigContextField';
|
||||
import ProjectDirContextField from './context/ProjectDirContextField';
|
||||
import SessionManagementContextField from './context/SessionManagementContextField';
|
||||
import { EasCommandError } from './errors';
|
||||
|
||||
type ContextInput<
|
||||
T extends {
|
||||
@@ -33,6 +36,8 @@ type ContextOutput<
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
protected static readonly ContextOptions = {
|
||||
/**
|
||||
@@ -103,6 +108,18 @@ export default abstract class EasCommand extends Command {
|
||||
*/
|
||||
static contextDefinition: ContextInput = {};
|
||||
|
||||
/**
|
||||
* The user session manager. Responsible for coordinating all user session related state.
|
||||
* If needed in a subclass, use the SessionManager ContextOption.
|
||||
*/
|
||||
private sessionManagerInternal?: SessionManager;
|
||||
|
||||
/**
|
||||
* The analytics manager. Used for logging analytics.
|
||||
* It is set up here to ensure a consistent setup.
|
||||
*/
|
||||
private analyticsInternal?: AnalyticsWithOrchestration;
|
||||
|
||||
/**
|
||||
* Execute the context in the contextDefinition to satisfy command prerequisites.
|
||||
*/
|
||||
@@ -132,20 +149,10 @@ export default abstract class EasCommand extends Command {
|
||||
return Object.fromEntries(contextValuePairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user session manager. Responsible for coordinating all user session related state.
|
||||
* If needed in a subclass, use the SessionManager ContextOption.
|
||||
*/
|
||||
private sessionManagerInternal?: SessionManager;
|
||||
private get sessionManager(): SessionManager {
|
||||
return nullthrows(this.sessionManagerInternal);
|
||||
}
|
||||
|
||||
/**
|
||||
* The analytics manager. Used for logging analytics.
|
||||
* It is set up here to ensure a consistent set up.
|
||||
*/
|
||||
private analyticsInternal?: AnalyticsWithOrchestration;
|
||||
private get analytics(): AnalyticsWithOrchestration {
|
||||
return nullthrows(this.analyticsInternal);
|
||||
}
|
||||
@@ -175,4 +182,19 @@ export default abstract class EasCommand extends Command {
|
||||
await this.analytics.flushAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
|
||||
protected override catch(err: Error): Promise<any> {
|
||||
let baseMessage = `${this.id} command failed.`;
|
||||
if (err instanceof EasCommandError) {
|
||||
Log.error(err.message);
|
||||
} else if (err instanceof CombinedError && err?.graphQLErrors) {
|
||||
const cleanMessage = err.message.replace('[GraphQL] ', '');
|
||||
Log.error(cleanMessage);
|
||||
baseMessage = BASE_GRAPHQL_ERROR_MESSAGE;
|
||||
} else {
|
||||
Log.error(err.message);
|
||||
}
|
||||
Log.debug(err);
|
||||
throw new Error(baseMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
|
||||
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
|
||||
import Log from '../../log';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
@@ -11,6 +14,7 @@ jest.mock('../../analytics/AnalyticsManager', () => {
|
||||
createAnalyticsAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../log');
|
||||
|
||||
let originalProcessArgv: string[];
|
||||
|
||||
@@ -40,7 +44,7 @@ const createTestEasCommand = (): typeof EasCommand => {
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
TestEasCommand.id = 'testEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
return TestEasCommand;
|
||||
};
|
||||
|
||||
@@ -96,5 +100,71 @@ describe(EasCommand.name, () => {
|
||||
|
||||
expect(analytics.flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('catch', () => {
|
||||
it('logs the message', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const logErrorSpy = jest.spyOn(Log, 'error');
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const error = new Error('Unexpected, internal error message');
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
try {
|
||||
await TestEasCommand.run();
|
||||
} catch {}
|
||||
|
||||
expect(logErrorSpy).toBeCalledWith('Unexpected, internal error message');
|
||||
expect(logDebugSpy).toBeCalledWith(error);
|
||||
});
|
||||
|
||||
it('logs the cleaned message if needed', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const logErrorSpy = jest.spyOn(Log, 'error');
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
const graphQLErrors = ['Unexpected GraphQL error message'];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
try {
|
||||
await TestEasCommand.run();
|
||||
} catch {}
|
||||
|
||||
expect(logErrorSpy).toBeCalledWith('Unexpected GraphQL error message');
|
||||
expect(logDebugSpy).toBeCalledWith(error);
|
||||
});
|
||||
|
||||
it('re-throws the error with default base message', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
throw new Error('Error message');
|
||||
});
|
||||
try {
|
||||
await TestEasCommand.run();
|
||||
} catch (caughtError) {
|
||||
expect(caughtError).toBeInstanceOf(Error);
|
||||
expect((caughtError as Error).message).toEqual('testEasCommand command failed.');
|
||||
}
|
||||
});
|
||||
|
||||
it('re-throws the error with a different default base message in case of graphQLError', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
|
||||
runAsyncMock.mockImplementation(() => {
|
||||
const graphQLErrors = ['Unexpected GraphQL error message'];
|
||||
throw new CombinedError({ graphQLErrors });
|
||||
});
|
||||
try {
|
||||
await TestEasCommand.run();
|
||||
} catch (caughtError) {
|
||||
expect(caughtError).toBeInstanceOf(Error);
|
||||
expect((caughtError as Error).message).toEqual('GraphQL request failed.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,8 @@ export class DynamicProjectConfigContextField extends ContextField<DynamicConfig
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
}: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
const expBefore = getExpoConfig(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
|
||||
+22
@@ -252,4 +252,26 @@ describe(getProjectIdAsync, () => {
|
||||
getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws if extra.eas.projectId is not a string', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: 1234 } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@notnotbrent/test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'notnotbrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: "extra.eas.projectId" must be a string, found number. If you're not sure how to set it up on your own, remove the property entirely and it will be automatically configured on the next EAS CLI run.`
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { getVcsClient, setVcsClient } from '../../../vcs';
|
||||
import GitClient from '../../../vcs/clients/git';
|
||||
|
||||
async function applyCliConfigAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -89,6 +89,12 @@ export async function getProjectIdAsync(
|
||||
|
||||
const localProjectId = exp.extra?.eas?.projectId;
|
||||
if (localProjectId) {
|
||||
if (typeof localProjectId !== 'string') {
|
||||
throw new Error(
|
||||
`Project config: "extra.eas.projectId" must be a string, found ${typeof localProjectId}. If you're not sure how to set it up on your own, remove the property entirely and it will be automatically configured on the next EAS CLI run.`
|
||||
);
|
||||
}
|
||||
|
||||
// check that the local project ID matches account and slug
|
||||
const appForProjectId = await AppQuery.byIdAsync(graphqlClient, localProjectId);
|
||||
if (exp.owner && exp.owner !== appForProjectId.ownerAccount.name) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export class EasCommandError extends Error {
|
||||
// constructor is not useless, since the constructor for Error allows for optional `message`
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
if (!(await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir)))) {
|
||||
return;
|
||||
}
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
|
||||
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
|
||||
[Platform.ANDROID, profileName],
|
||||
|
||||
@@ -6,6 +6,8 @@ import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { setVcsClient } from '../../vcs';
|
||||
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
|
||||
|
||||
/**
|
||||
* This command will be run on the EAS Build workers, when building
|
||||
@@ -51,6 +53,8 @@ export default class BuildInternal extends EasCommand {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
setVcsClient(new GitNoCommitClient());
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await runBuildAndSubmitAsync(
|
||||
|
||||
@@ -121,7 +121,7 @@ export default class BuildResign extends EasCommand {
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
});
|
||||
|
||||
const platform = await selectPlatformAsync(flags.platform);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
|
||||
const profile = await EasJsonUtils.getBuildProfileAsync(
|
||||
easJsonAccessor,
|
||||
|
||||
@@ -71,7 +71,7 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
});
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
|
||||
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import { BranchMapping, getBranchMapping } from '../../channel/utils';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
@@ -132,13 +134,11 @@ async function startRolloutAsync(
|
||||
);
|
||||
}
|
||||
|
||||
const logMessage = `️Started a rollout of branch ${chalk.bold(
|
||||
const logMessage = `Started a rollout of branch ${chalk.bold(branchName)} on channel ${chalk.bold(
|
||||
channelName!
|
||||
)}! ${chalk.bold(percent)}% of users will be directed to branch ${chalk.bold(
|
||||
branchName
|
||||
)} onto channel ${chalk.bold(channelName!)}! ${chalk.bold(
|
||||
percent
|
||||
)}% of users will be directed to branch ${chalk.bold(branchName)}, ${chalk.bold(
|
||||
100 - percent
|
||||
)}% to branch ${chalk.bold(oldBranch.name)}.`;
|
||||
)}, ${chalk.bold(100 - percent)}% to branch ${chalk.bold(oldBranch.name)}.`;
|
||||
|
||||
return { newChannelInfo, logMessage };
|
||||
}
|
||||
@@ -190,7 +190,7 @@ async function editRolloutAsync(
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const logMessage = `️Rollout of branch ${chalk.bold(newBranch.name)} onto channel ${chalk.bold(
|
||||
const logMessage = `Rollout of branch ${chalk.bold(newBranch.name)} on channel ${chalk.bold(
|
||||
channelName!
|
||||
)} updated from ${chalk.bold(currentPercent)}% to ${chalk.bold(percent)}%. ${chalk.bold(
|
||||
percent
|
||||
@@ -256,11 +256,11 @@ async function endRolloutAsync(
|
||||
'Ending the rollout will send all traffic to a single branch. Which one should that be?',
|
||||
[
|
||||
{
|
||||
title: `${newBranch.name} ${chalk.grey(`- current percent: ${100 - currentPercent}%`)}`,
|
||||
title: `${newBranch.name} ${chalk.grey(`- current percent: ${currentPercent}%`)}`,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
title: `${oldBranch.name} ${chalk.grey(`- current percent: ${currentPercent}%`)}`,
|
||||
title: `${oldBranch.name} ${chalk.grey(`- current percent: ${100 - currentPercent}%`)}`,
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
@@ -284,7 +284,7 @@ async function endRolloutAsync(
|
||||
channelId: channel.id,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
const logMessage = `️Rollout on channel ${chalk.bold(
|
||||
const logMessage = `Rollout on channel ${chalk.bold(
|
||||
channelName
|
||||
)} ended. All traffic is now sent to branch ${chalk.bold(
|
||||
endOnNewBranch ? newBranch.name : oldBranch.name
|
||||
@@ -294,14 +294,12 @@ async function endRolloutAsync(
|
||||
}
|
||||
|
||||
export default class ChannelRollout extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description = 'Rollout a new branch out to a channel incrementally.';
|
||||
static override description = 'Roll a new branch out on a channel incrementally.';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'channel',
|
||||
required: true,
|
||||
description: 'rollout that the channel is on',
|
||||
description: 'channel on which the rollout should be done',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -311,7 +309,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
required: false,
|
||||
}),
|
||||
percent: Flags.integer({
|
||||
description: 'percent of traffic to redirect to the new branch',
|
||||
description: 'percent of users to send to the new branch',
|
||||
required: false,
|
||||
}),
|
||||
end: Flags.boolean({
|
||||
@@ -328,7 +326,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { channel: channelName },
|
||||
args: { channel: channelNameArg },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
end: endFlag,
|
||||
@@ -349,9 +347,23 @@ export default class ChannelRollout extends EasCommand {
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
let channelName: string = channelNameArg;
|
||||
if (!channelName) {
|
||||
const { name } = await selectChannelOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
selectionPromptTitle: 'Select a channel on which to perform a rollout',
|
||||
paginatedQueryOptions: {
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
channelName = name;
|
||||
}
|
||||
|
||||
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: channelName!,
|
||||
channelName,
|
||||
});
|
||||
if (!channel) {
|
||||
throw new Error(
|
||||
@@ -396,7 +408,15 @@ export default class ChannelRollout extends EasCommand {
|
||||
if (!isRollout) {
|
||||
rolloutMutationResult = await startRolloutAsync(graphqlClient, {
|
||||
channelName,
|
||||
branchName: branchName ?? (await promptForBranchNameAsync(channelName, nonInteractive)),
|
||||
branchName:
|
||||
branchName ??
|
||||
(await promptForBranchNameAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelName,
|
||||
nonInteractive,
|
||||
json: jsonFlag,
|
||||
})),
|
||||
percent,
|
||||
nonInteractive,
|
||||
projectId,
|
||||
@@ -433,18 +453,36 @@ export default class ChannelRollout extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForBranchNameAsync(
|
||||
channelName: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<string> {
|
||||
async function promptForBranchNameAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelName,
|
||||
nonInteractive,
|
||||
json,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
channelName: string;
|
||||
nonInteractive: boolean;
|
||||
json: boolean;
|
||||
}): Promise<string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply branch flag in non-interactive mode');
|
||||
}
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `Select a branch to rollout onto ${channelName}`,
|
||||
validate: value => (value ? true : 'A branch must be specified.'),
|
||||
|
||||
const { name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like roll out on ${channelName}?`,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: updateBranch.name,
|
||||
}),
|
||||
// discard limit and offset because this query is not their intended target
|
||||
paginatedQueryOptions: {
|
||||
json,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
return name;
|
||||
|
||||
return branchName;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default class Config extends EasCommand {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor(projectDir);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const profileName =
|
||||
maybeProfile ??
|
||||
(await selectAsync(
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class MetadataLint extends EasCommand {
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class MetadataPull extends EasCommand {
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class MetadataPush extends EasCommand {
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ export default class Submit extends EasCommand {
|
||||
const platforms = toPlatforms(flagsWithPlatform.requestedPlatform);
|
||||
const submissionProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms,
|
||||
profileName: flagsWithPlatform.profile,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
|
||||
import {
|
||||
AppJSONConfig,
|
||||
ExpoConfig,
|
||||
GetConfigOptions,
|
||||
PackageJSONConfig,
|
||||
Platform,
|
||||
getConfig,
|
||||
} from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { vol } from 'memfs';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
@@ -29,6 +37,7 @@ const updateStub: UpdateFragment = {
|
||||
platform: 'ios',
|
||||
gitCommitHash: 'commit',
|
||||
manifestFragment: JSON.stringify({ fake: 'manifest' }),
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
@@ -58,7 +67,10 @@ jest.mock('../../../project/publish', () => ({
|
||||
}));
|
||||
|
||||
describe(UpdatePublish.name, () => {
|
||||
afterEach(() => vol.reset());
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockClear();
|
||||
});
|
||||
|
||||
// Deprecated and split to a new command: update:republish
|
||||
it('errors with --republish', async () => {
|
||||
@@ -80,7 +92,7 @@ describe(UpdatePublish.name, () => {
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -136,14 +148,59 @@ describe(UpdatePublish.name, () => {
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a new update with the public expo config', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
// Add configuration to the project that should not be included in the update
|
||||
const { appJson } = mockTestProject({
|
||||
expoConfig: {
|
||||
hooks: {
|
||||
postPublish: [
|
||||
{
|
||||
file: 'custom-hook.js',
|
||||
config: { some: 'config' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { platforms, runtimeVersion } = mockTestExport({ platforms: ['ios'] });
|
||||
|
||||
// Mock an existing branch, so we don't create a new one
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
// Pull the publish data from the mocked publish function
|
||||
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
|
||||
// Pull the Expo config from the publish data
|
||||
const expoConfig = nullthrows(publishData.updateInfoGroup).ios!.extra.expoClient;
|
||||
|
||||
// Ensure the basic properties are present
|
||||
expect(expoConfig).toHaveProperty('name', appJson.expo.name);
|
||||
expect(expoConfig).toHaveProperty('slug', appJson.expo.slug);
|
||||
// Ensure non-public config is not present
|
||||
expect(expoConfig).not.toHaveProperty('hooks');
|
||||
});
|
||||
});
|
||||
|
||||
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
|
||||
function mockTestProject({
|
||||
configuredProjectId = '1234',
|
||||
expoConfig = {},
|
||||
}: {
|
||||
configuredProjectId?: string;
|
||||
} = {}): { projectId: string } {
|
||||
expoConfig?: Partial<ExpoConfig>;
|
||||
} = {}): { projectId: string; appJson: AppJSONConfig } {
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
@@ -163,6 +220,7 @@ function mockTestProject({
|
||||
projectId: configuredProjectId,
|
||||
},
|
||||
},
|
||||
...expoConfig,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,11 +238,27 @@ function mockTestProject({
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest
|
||||
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
|
||||
.mockResolvedValue(async () => ({
|
||||
exp: mockManifest.exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
}));
|
||||
.mockResolvedValue(async (options?: GetConfigOptions) => {
|
||||
let exp = { ...mockManifest.exp };
|
||||
|
||||
// Fake a limited public config
|
||||
if (options?.isPublicConfig) {
|
||||
exp = {
|
||||
name: mockManifest.exp.name,
|
||||
version: mockManifest.exp.version,
|
||||
slug: mockManifest.exp.slug,
|
||||
sdkVersion: mockManifest.exp.sdkVersion,
|
||||
owner: mockManifest.exp.owner,
|
||||
extra: mockManifest.exp.extra,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
};
|
||||
});
|
||||
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
@@ -199,7 +273,7 @@ function mockTestProject({
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
return { projectId: configuredProjectId };
|
||||
return { projectId: configuredProjectId, appJson: appJSON };
|
||||
}
|
||||
|
||||
/** Create a new in-memory export of the project */
|
||||
|
||||
@@ -27,6 +27,7 @@ const updateStub: UpdateFragment = {
|
||||
platform: 'ios',
|
||||
gitCommitHash: 'commit',
|
||||
manifestFragment: JSON.stringify({ fake: 'manifest' }),
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import {
|
||||
AppJSONConfig,
|
||||
ExpoConfig,
|
||||
GetConfigOptions,
|
||||
PackageJSONConfig,
|
||||
getConfig,
|
||||
} from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { vol } from 'memfs';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { ensureBranchExistsAsync } from '../../../branch/queries';
|
||||
import { DynamicProjectConfigContextField } from '../../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { UpdateFragment } from '../../../graphql/generated';
|
||||
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
|
||||
import UpdateRollBackToEmbedded from '../roll-back-to-embedded';
|
||||
|
||||
const projectRoot = '/test-project';
|
||||
const commandOptions = { root: projectRoot } as any;
|
||||
const updateStub: UpdateFragment = {
|
||||
id: 'update-1234',
|
||||
group: 'group-1234',
|
||||
branch: { id: 'branch-1234', name: 'main' },
|
||||
message: 'test message',
|
||||
runtimeVersion: 'exposdk:47.0.0',
|
||||
platform: 'ios',
|
||||
gitCommitHash: 'commit',
|
||||
manifestFragment: JSON.stringify({ fake: 'manifest' }),
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
};
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('@expo/config-plugins');
|
||||
jest.mock('../../../branch/queries');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
|
||||
jest.mock('../../../update/configure');
|
||||
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
|
||||
jest.mock('../../../graphql/mutations/PublishMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/UpdateQuery');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/publish', () => ({
|
||||
...jest.requireActual('../../../project/publish'),
|
||||
buildBundlesAsync: jest.fn(),
|
||||
collectAssetsAsync: jest.fn(),
|
||||
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
|
||||
uploadAssetsAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
describe(UpdateRollBackToEmbedded.name, () => {
|
||||
afterEach(() => vol.reset());
|
||||
|
||||
it('errors with both --channel and --branch', async () => {
|
||||
const flags = ['--channel=channel123', '--branch=branch123'];
|
||||
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdateRollBackToEmbedded(flags, commandOptions).run()).rejects.toThrow(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with --non-interactive, --branch, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
mockTestProject();
|
||||
const platforms = ['android', 'ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with --non-interactive, --channel, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
|
||||
|
||||
const { projectId } = mockTestProject();
|
||||
const platforms = ['android', 'ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
|
||||
platforms.map(platform => ({
|
||||
...updateStub,
|
||||
runtimeVersion,
|
||||
platform,
|
||||
}))
|
||||
);
|
||||
|
||||
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
|
||||
|
||||
expect(ensureBranchExistsAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
{
|
||||
appId: projectId,
|
||||
branchName: 'branchFromChannel',
|
||||
}
|
||||
);
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with the public expo config', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
// Add configuration to the project that should not be included in the update
|
||||
mockTestProject({
|
||||
expoConfig: {
|
||||
hooks: {
|
||||
postPublish: [
|
||||
{
|
||||
file: 'custom-hook.js',
|
||||
config: { some: 'config' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const platforms = ['ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
// Mock an existing branch, so we don't create a new one
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
|
||||
|
||||
// Pull the publish data from the mocked publish function
|
||||
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
|
||||
expect(nullthrows(publishData.rollBackToEmbeddedInfoGroup).ios).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
|
||||
function mockTestProject({
|
||||
configuredProjectId = '1234',
|
||||
expoConfig = {},
|
||||
}: {
|
||||
configuredProjectId?: string;
|
||||
expoConfig?: Partial<ExpoConfig>;
|
||||
} = {}): { projectId: string; appJson: AppJSONConfig } {
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const appJSON: AppJSONConfig = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: jester.accounts[0].name,
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: configuredProjectId,
|
||||
},
|
||||
},
|
||||
...expoConfig,
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'package.json': JSON.stringify(packageJSON),
|
||||
'app.json': JSON.stringify(appJSON),
|
||||
},
|
||||
projectRoot
|
||||
);
|
||||
|
||||
const mockManifest = { exp: appJSON.expo };
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest
|
||||
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
|
||||
.mockResolvedValue(async (options?: GetConfigOptions) => {
|
||||
let exp = { ...mockManifest.exp };
|
||||
|
||||
// Fake a limited public config
|
||||
if (options?.isPublicConfig) {
|
||||
exp = {
|
||||
name: mockManifest.exp.name,
|
||||
version: mockManifest.exp.version,
|
||||
slug: mockManifest.exp.slug,
|
||||
sdkVersion: mockManifest.exp.sdkVersion,
|
||||
owner: mockManifest.exp.owner,
|
||||
extra: mockManifest.exp.extra,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
};
|
||||
});
|
||||
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
});
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
return { projectId: configuredProjectId, appJson: appJSON };
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Platform as PublishPlatform } from '@expo/config';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import { ensureChannelExistsAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -23,32 +19,31 @@ import {
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
ExpoCLIExportPlatformFlag,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
defaultPublishPlatforms,
|
||||
filterExportedPlatformsByFlag,
|
||||
getBranchNameForCommandAsync,
|
||||
getRequestedPlatform,
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions,
|
||||
getRuntimeVersionObjectAsync,
|
||||
getUpdateMessageForCommandAsync,
|
||||
isUploadedAssetCountAboveWarningThreshold,
|
||||
resolveInputDirectoryAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
|
||||
import {
|
||||
formatUpdateMessage,
|
||||
getUpdateGroupJsonInfo,
|
||||
truncateString as truncateUpdateMessage,
|
||||
} from '../../update/utils';
|
||||
import { getUpdateGroupJsonInfo } from '../../update/utils';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getManifestBodyAsync,
|
||||
signManifestBody,
|
||||
signBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
@@ -56,8 +51,6 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
|
||||
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
@@ -88,21 +81,6 @@ type UpdateFlags = {
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
function getRequestedPlatform(platform: ExpoCLIExportPlatformFlag): RequestedPlatform | null {
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
return RequestedPlatform.Android;
|
||||
case 'ios':
|
||||
return RequestedPlatform.Ios;
|
||||
case 'web':
|
||||
return null;
|
||||
case 'all':
|
||||
return RequestedPlatform.All;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
static override description = 'publish an update group';
|
||||
|
||||
@@ -166,18 +144,18 @@ export default class UpdatePublish extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(UpdatePublish);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
let {
|
||||
const {
|
||||
auto: autoFlag,
|
||||
platform: platformFlag,
|
||||
channelName,
|
||||
updateMessage,
|
||||
channelName: channelNameArg,
|
||||
updateMessage: updateMessageArg,
|
||||
inputDir,
|
||||
skipBundler,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
branchName: branchNameArg,
|
||||
} = this.sanitizeFlags(rawFlags);
|
||||
let branchName = this.sanitizeFlags(rawFlags).branchName;
|
||||
|
||||
const {
|
||||
getDynamicProjectConfigAsync,
|
||||
@@ -198,10 +176,6 @@ export default class UpdatePublish extends EasCommand {
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: false,
|
||||
});
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
@@ -211,75 +185,28 @@ export default class UpdatePublish extends EasCommand {
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync({});
|
||||
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
|
||||
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: false,
|
||||
});
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
const branchName = await getBranchNameForCommandAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelNameArg,
|
||||
branchNameArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
|
||||
if (channelName && branchName) {
|
||||
throw new Error(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
|
||||
);
|
||||
}
|
||||
|
||||
if (channelName) {
|
||||
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
if (autoFlag) {
|
||||
branchName = await getDefaultBranchNameAsync();
|
||||
} else if (nonInteractive) {
|
||||
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
|
||||
} else {
|
||||
try {
|
||||
const branch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like to publish on?`,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: `${updateBranch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
|
||||
)}`,
|
||||
}),
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
branchName = branch.name;
|
||||
} catch {
|
||||
// unable to select a branch (network error or no branches for project)
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Provide a branch name:',
|
||||
initial: await getDefaultBranchNameAsync(),
|
||||
validate: value => (value ? true : 'Branch name may not be empty.'),
|
||||
}));
|
||||
}
|
||||
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateMessage && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
const updateMessage = await getUpdateMessageForCommandAsync({
|
||||
updateMessageArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
});
|
||||
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
@@ -300,6 +227,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
|
||||
try {
|
||||
const collectedAssets = await collectAssetsAsync(distRoot);
|
||||
@@ -351,22 +279,9 @@ export default class UpdatePublish extends EasCommand {
|
||||
throw e;
|
||||
}
|
||||
|
||||
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
|
||||
if (truncatedMessage !== updateMessage) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
|
||||
|
||||
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
|
||||
for (const runtime of runtimeVersions) {
|
||||
const platforms = runtimeVersions
|
||||
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
|
||||
.map(({ platform }) => platform);
|
||||
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
|
||||
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
|
||||
}
|
||||
}
|
||||
const runtimeToPlatformMapping =
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
|
||||
|
||||
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
@@ -401,7 +316,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion,
|
||||
message: truncatedMessage,
|
||||
message: updateMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
@@ -418,7 +333,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
const updatesTemp = [...newUpdates];
|
||||
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
|
||||
const newUpdates = updatesTemp.splice(0, Object.keys(updateGroup.updateInfoGroup).length);
|
||||
const newUpdates = updatesTemp.splice(
|
||||
0,
|
||||
Object.keys(nullthrows(updateGroup.updateInfoGroup)).length
|
||||
);
|
||||
return {
|
||||
updateGroup,
|
||||
newUpdates,
|
||||
@@ -438,11 +356,13 @@ export default class UpdatePublish extends EasCommand {
|
||||
checkManifestBodyAgainstUpdateInfoGroup(
|
||||
manifestBody,
|
||||
nullthrows(
|
||||
updateGroup.updateInfoGroup[newUpdate.platform as keyof UpdateInfoGroup]
|
||||
nullthrows(updateGroup.updateInfoGroup)[
|
||||
newUpdate.platform as keyof UpdateInfoGroup
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
const manifestSignature = signManifestBody(manifestBody, codeSigningInfo);
|
||||
const manifestSignature = signBody(manifestBody, codeSigningInfo);
|
||||
|
||||
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
|
||||
alg: codeSigningInfo.codeSigningMetadata.alg,
|
||||
@@ -504,7 +424,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: truncatedMessage },
|
||||
{ label: 'Message', value: updateMessage },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
@@ -575,41 +495,3 @@ export default class UpdatePublish extends EasCommand {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platforms: PublishPlatform[],
|
||||
projectDir: string
|
||||
): Promise<{ platform: string; runtimeVersion: string }[]> {
|
||||
for (const platform of platforms) {
|
||||
if (platform === 'web') {
|
||||
continue;
|
||||
}
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged =
|
||||
(await resolveWorkflowAsync(projectDir, platform as Platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error(
|
||||
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(platforms)].map(platform => {
|
||||
if (platform === 'web') {
|
||||
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
|
||||
}
|
||||
return {
|
||||
platform,
|
||||
runtimeVersion: nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,7 @@ type UpdateToRepublish = {
|
||||
>;
|
||||
|
||||
export default class UpdateRepublish extends EasCommand {
|
||||
static override description = 'rollback to an existing update';
|
||||
static override description = 'roll back to an existing update';
|
||||
|
||||
static override flags = {
|
||||
channel: Flags.string({
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
import { Platform as PublishPlatform } from '@expo/config';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import { ensureChannelExistsAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
PublishUpdateGroupInput,
|
||||
StatuspageServiceName,
|
||||
UpdatePublishMutation,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
ExpoCLIExportPlatformFlag,
|
||||
defaultPublishPlatforms,
|
||||
getBranchNameForCommandAsync,
|
||||
getRequestedPlatform,
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions,
|
||||
getRuntimeVersionObjectAsync,
|
||||
getUpdateMessageForCommandAsync,
|
||||
} from '../../project/publish';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getUpdateGroupJsonInfo } from '../../update/utils';
|
||||
import {
|
||||
CodeSigningInfo,
|
||||
checkDirectiveBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getDirectiveBodyAsync,
|
||||
signBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
channel?: string;
|
||||
message?: string;
|
||||
platform: string;
|
||||
'private-key-path'?: string;
|
||||
'non-interactive': boolean;
|
||||
json: boolean;
|
||||
};
|
||||
|
||||
type UpdateFlags = {
|
||||
auto: boolean;
|
||||
platform: ExpoCLIExportPlatformFlag;
|
||||
branchName?: string;
|
||||
channelName?: string;
|
||||
updateMessage?: string;
|
||||
privateKeyPath?: string;
|
||||
json: boolean;
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
static override hidden = true; // until we launch
|
||||
static override description = 'roll back to the embedded update';
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
description: 'Branch to publish the rollback to embedded update group on',
|
||||
required: false,
|
||||
}),
|
||||
channel: Flags.string({
|
||||
description: 'Channel that the published rollback to embedded update should affect',
|
||||
required: false,
|
||||
}),
|
||||
message: Flags.string({
|
||||
description: 'A short message describing the rollback to embedded update',
|
||||
required: false,
|
||||
}),
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
auto: Flags.boolean({
|
||||
description:
|
||||
'Use the current git branch and commit message for the EAS branch and update message',
|
||||
default: false,
|
||||
}),
|
||||
'private-key-path': Flags.string({
|
||||
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
|
||||
required: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(UpdateRollBackToEmbedded);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
const {
|
||||
auto: autoFlag,
|
||||
platform: platformFlag,
|
||||
channelName: channelNameArg,
|
||||
updateMessage: updateMessageArg,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
branchName: branchNameArg,
|
||||
} = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const {
|
||||
getDynamicProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(UpdateRollBackToEmbedded, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
projectId,
|
||||
projectDir,
|
||||
} = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
platform: getRequestedPlatform(platformFlag),
|
||||
projectDir,
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
|
||||
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: false,
|
||||
});
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
const branchName = await getBranchNameForCommandAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelNameArg,
|
||||
branchNameArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
|
||||
const updateMessage = await getUpdateMessageForCommandAsync({
|
||||
updateMessageArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
});
|
||||
|
||||
const realizedPlatforms: PublishPlatform[] =
|
||||
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
|
||||
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchName,
|
||||
});
|
||||
if (createdBranch) {
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchId,
|
||||
channelName: branchName,
|
||||
});
|
||||
}
|
||||
|
||||
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
|
||||
|
||||
const vcsClient = getVcsClient();
|
||||
|
||||
const gitCommitHash = await vcsClient.getCommitHashAsync();
|
||||
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
|
||||
|
||||
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdates = await this.publishRollbacksAsync({
|
||||
graphqlClient,
|
||||
isGitWorkingTreeDirty,
|
||||
gitCommitHash,
|
||||
updateMessage,
|
||||
branchId,
|
||||
codeSigningInfo,
|
||||
runtimeVersions,
|
||||
realizedPlatforms,
|
||||
});
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to publish updates');
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(getUpdateGroupJsonInfo(newUpdates));
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
|
||||
const newUpdatesForRuntimeVersion = newUpdates.filter(
|
||||
update => update.runtimeVersion === runtime.runtimeVersion
|
||||
);
|
||||
if (newUpdatesForRuntimeVersion.length === 0) {
|
||||
throw new Error(
|
||||
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
|
||||
);
|
||||
}
|
||||
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
|
||||
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
|
||||
update => update.platform === 'android'
|
||||
);
|
||||
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
|
||||
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
|
||||
|
||||
const projectName = exp.slug;
|
||||
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
|
||||
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
|
||||
const updateGroupLink = link(updateGroupUrl, { dim: false });
|
||||
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: runtime.runtimeVersion },
|
||||
{ label: 'Platform', value: platforms.join(', ') },
|
||||
{ label: 'Update group ID', value: updateGroupId },
|
||||
...(newAndroidUpdate
|
||||
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: updateMessage },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
label: 'Commit',
|
||||
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Website link', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async publishRollbacksAsync({
|
||||
graphqlClient,
|
||||
isGitWorkingTreeDirty,
|
||||
gitCommitHash,
|
||||
updateMessage,
|
||||
branchId,
|
||||
codeSigningInfo,
|
||||
runtimeVersions,
|
||||
realizedPlatforms,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
isGitWorkingTreeDirty: boolean | undefined;
|
||||
gitCommitHash: string | undefined;
|
||||
updateMessage: string;
|
||||
branchId: string;
|
||||
codeSigningInfo: CodeSigningInfo | undefined;
|
||||
runtimeVersions: { platform: string; runtimeVersion: string }[];
|
||||
realizedPlatforms: PublishPlatform[];
|
||||
}): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
|
||||
const runtimeToPlatformMapping =
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
|
||||
const rollbackInfoGroups = Object.fromEntries(
|
||||
realizedPlatforms.map(platform => [platform, true])
|
||||
);
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
|
||||
({ runtimeVersion, platforms }) => {
|
||||
const localRollbackInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
|
||||
);
|
||||
|
||||
return {
|
||||
branchId,
|
||||
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
|
||||
runtimeVersion,
|
||||
message: updateMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const newUpdates = await PublishMutation.publishUpdateGroupAsync(graphqlClient, updateGroups);
|
||||
|
||||
if (codeSigningInfo) {
|
||||
Log.log('🔒 Signing roll back');
|
||||
|
||||
const updatesTemp = [...newUpdates];
|
||||
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
|
||||
const newUpdates = updatesTemp.splice(
|
||||
0,
|
||||
Object.keys(nullthrows(updateGroup.rollBackToEmbeddedInfoGroup)).length
|
||||
);
|
||||
return {
|
||||
updateGroup,
|
||||
newUpdates,
|
||||
};
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
updateGroupsAndTheirUpdates.map(async ({ newUpdates }) => {
|
||||
await Promise.all(
|
||||
newUpdates.map(async newUpdate => {
|
||||
const response = await fetch(newUpdate.manifestPermalink, {
|
||||
method: 'GET',
|
||||
headers: { accept: 'multipart/mixed' },
|
||||
});
|
||||
const directiveBody = nullthrows(await getDirectiveBodyAsync(response));
|
||||
|
||||
checkDirectiveBodyAgainstUpdateInfoGroup(directiveBody);
|
||||
|
||||
const directiveSignature = signBody(directiveBody, codeSigningInfo);
|
||||
|
||||
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
|
||||
alg: codeSigningInfo.codeSigningMetadata.alg,
|
||||
keyid: codeSigningInfo.codeSigningMetadata.keyid,
|
||||
sig: directiveSignature,
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return newUpdates;
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
|
||||
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
|
||||
Errors.error(
|
||||
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
auto,
|
||||
branchName,
|
||||
channelName,
|
||||
updateMessage,
|
||||
platform: flags.platform as RequestedPlatform,
|
||||
privateKeyPath: flags['private-key-path'],
|
||||
nonInteractive,
|
||||
json: flags.json ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export default class IosCredentialsProvider {
|
||||
private async disablePushNotificationsSetupInEasJsonAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(ctx.projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(ctx.projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
easJsonRawObject.cli = {
|
||||
|
||||
@@ -8,7 +8,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
private easJsonAccessor: EasJsonAccessor;
|
||||
|
||||
constructor(projectDir: string, private platform: T) {
|
||||
this.easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
this.easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
}
|
||||
|
||||
async runAsync(): Promise<BuildProfile<T>> {
|
||||
|
||||
+163
-33
@@ -500,6 +500,13 @@ export type AccountSsoConfigurationPublicDataQueryPublicDataByAccountNameArgs =
|
||||
accountName: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AccountUsageEasBuildMetadata = {
|
||||
__typename?: 'AccountUsageEASBuildMetadata';
|
||||
platform: AppPlatform;
|
||||
};
|
||||
|
||||
export type AccountUsageMetadata = AccountUsageEasBuildMetadata;
|
||||
|
||||
export type AccountUsageMetric = {
|
||||
__typename?: 'AccountUsageMetric';
|
||||
id: Scalars['ID'];
|
||||
@@ -509,19 +516,6 @@ export type AccountUsageMetric = {
|
||||
value: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type AccountUsageMetricAndCost = {
|
||||
__typename?: 'AccountUsageMetricAndCost';
|
||||
id: Scalars['ID'];
|
||||
/** The limit, in units, allowed by this plan */
|
||||
limit: Scalars['Float'];
|
||||
metricType: UsageMetricType;
|
||||
service: EasService;
|
||||
serviceMetric: EasServiceMetric;
|
||||
/** Total cost of this particular metric, in cents */
|
||||
totalCost: Scalars['Float'];
|
||||
value: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type AccountUsageMetrics = {
|
||||
__typename?: 'AccountUsageMetrics';
|
||||
byBillingPeriod: UsageMetricTotal;
|
||||
@@ -954,8 +948,10 @@ export type App = Project & {
|
||||
buildOrBuildJobs: Array<BuildOrBuildJob>;
|
||||
/** (EAS Build) Builds associated with this app */
|
||||
builds: Array<Build>;
|
||||
buildsPaginated: AppBuildsConnection;
|
||||
/** Classic update release channel names that have at least one build */
|
||||
buildsReleaseChannels: Array<Scalars['String']>;
|
||||
channelsPaginated: AppChannelsConnection;
|
||||
deployment?: Maybe<Deployment>;
|
||||
/** Deployments associated with this app */
|
||||
deployments: DeploymentsConnection;
|
||||
@@ -1011,6 +1007,7 @@ export type App = Project & {
|
||||
slug: Scalars['String'];
|
||||
/** EAS Submissions associated with this app */
|
||||
submissions: Array<Submission>;
|
||||
submissionsPaginated: AppSubmissionsConnection;
|
||||
/** Coalesced project activity for an app using pagination */
|
||||
timelineActivity: TimelineActivityConnection;
|
||||
/** @deprecated 'likes' have been deprecated. */
|
||||
@@ -1090,6 +1087,25 @@ export type AppBuildsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppBuildsPaginatedArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
before?: InputMaybe<Scalars['String']>;
|
||||
filter?: InputMaybe<BuildFilterInput>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppChannelsPaginatedArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
before?: InputMaybe<Scalars['String']>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppDeploymentArgs = {
|
||||
channel: Scalars['String'];
|
||||
@@ -1147,6 +1163,15 @@ export type AppSubmissionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppSubmissionsPaginatedArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
before?: InputMaybe<Scalars['String']>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppTimelineActivityArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
@@ -1224,6 +1249,30 @@ export type AppBranchesConnection = {
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AppBuildEdge = {
|
||||
__typename?: 'AppBuildEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: BuildOrBuildJob;
|
||||
};
|
||||
|
||||
export type AppBuildsConnection = {
|
||||
__typename?: 'AppBuildsConnection';
|
||||
edges: Array<AppBuildEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AppChannelEdge = {
|
||||
__typename?: 'AppChannelEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: UpdateChannel;
|
||||
};
|
||||
|
||||
export type AppChannelsConnection = {
|
||||
__typename?: 'AppChannelsConnection';
|
||||
edges: Array<AppChannelEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AppDataInput = {
|
||||
id: Scalars['ID'];
|
||||
privacy?: InputMaybe<Scalars['String']>;
|
||||
@@ -1413,6 +1462,18 @@ export enum AppStoreConnectUserRole {
|
||||
Unknown = 'UNKNOWN'
|
||||
}
|
||||
|
||||
export type AppSubmissionEdge = {
|
||||
__typename?: 'AppSubmissionEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: Submission;
|
||||
};
|
||||
|
||||
export type AppSubmissionsConnection = {
|
||||
__typename?: 'AppSubmissionsConnection';
|
||||
edges: Array<AppSubmissionEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AppUpdateEdge = {
|
||||
__typename?: 'AppUpdateEdge';
|
||||
cursor: Scalars['String'];
|
||||
@@ -1845,11 +1906,6 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
__typename?: 'Build';
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
actor?: Maybe<Actor>;
|
||||
/**
|
||||
* The actual resource class of the builder assigned to the build job
|
||||
* @deprecated Use resourceClassDisplayName instead
|
||||
*/
|
||||
actualResourceClass?: Maybe<BuildResourceClass>;
|
||||
appBuildVersion?: Maybe<Scalars['String']>;
|
||||
appVersion?: Maybe<Scalars['String']>;
|
||||
artifacts?: Maybe<BuildArtifacts>;
|
||||
@@ -1961,6 +2017,12 @@ export type BuildFilter = {
|
||||
status?: InputMaybe<BuildStatus>;
|
||||
};
|
||||
|
||||
export type BuildFilterInput = {
|
||||
channel?: InputMaybe<Scalars['String']>;
|
||||
platforms?: InputMaybe<Array<AppPlatform>>;
|
||||
releaseChannel?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum BuildIosEnterpriseProvisioning {
|
||||
Adhoc = 'ADHOC',
|
||||
Universal = 'UNIVERSAL'
|
||||
@@ -2174,7 +2236,9 @@ export type BuildOrBuildJobQueryByIdArgs = {
|
||||
};
|
||||
|
||||
export type BuildParamsInput = {
|
||||
reactNativeVersion?: InputMaybe<Scalars['String']>;
|
||||
resourceClass: BuildResourceClass;
|
||||
sdkVersion?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum BuildPriority {
|
||||
@@ -2261,8 +2325,6 @@ export enum BuildResourceClass {
|
||||
/** @deprecated Use IOS_M_MEDIUM instead */
|
||||
IosM1Large = 'IOS_M1_LARGE',
|
||||
IosM1Medium = 'IOS_M1_MEDIUM',
|
||||
IosM2Medium = 'IOS_M2_MEDIUM',
|
||||
IosM2ProMedium = 'IOS_M2_PRO_MEDIUM',
|
||||
IosMedium = 'IOS_MEDIUM',
|
||||
IosMLarge = 'IOS_M_LARGE',
|
||||
IosMMedium = 'IOS_M_MEDIUM',
|
||||
@@ -2464,6 +2526,11 @@ export type DeleteAppleProvisioningProfileResult = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type DeleteDiscordUserResult = {
|
||||
__typename?: 'DeleteDiscordUserResult';
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type DeleteEnvironmentSecretResult = {
|
||||
__typename?: 'DeleteEnvironmentSecretResult';
|
||||
id: Scalars['ID'];
|
||||
@@ -2558,6 +2625,32 @@ export type DeploymentsConnection = {
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type DiscordUser = {
|
||||
__typename?: 'DiscordUser';
|
||||
discordIdentifier: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
metadata?: Maybe<DiscordUserMetadata>;
|
||||
userActor: UserActor;
|
||||
};
|
||||
|
||||
export type DiscordUserMetadata = {
|
||||
__typename?: 'DiscordUserMetadata';
|
||||
discordAvatarUrl: Scalars['String'];
|
||||
discordDiscriminator: Scalars['String'];
|
||||
discordUsername: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DiscordUserMutation = {
|
||||
__typename?: 'DiscordUserMutation';
|
||||
/** Delete a Discord User by ID */
|
||||
deleteDiscordUser: DeleteDiscordUserResult;
|
||||
};
|
||||
|
||||
|
||||
export type DiscordUserMutationDeleteDiscordUserArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export enum DistributionType {
|
||||
Internal = 'INTERNAL',
|
||||
Simulator = 'SIMULATOR',
|
||||
@@ -2664,6 +2757,30 @@ export enum EnvironmentSecretType {
|
||||
String = 'STRING'
|
||||
}
|
||||
|
||||
export type EstimatedOverageAndCost = {
|
||||
__typename?: 'EstimatedOverageAndCost';
|
||||
id: Scalars['ID'];
|
||||
/** The limit, in units, allowed by this plan */
|
||||
limit: Scalars['Float'];
|
||||
metadata?: Maybe<AccountUsageMetadata>;
|
||||
metricType: UsageMetricType;
|
||||
service: EasService;
|
||||
serviceMetric: EasServiceMetric;
|
||||
/** Total cost of this particular metric, in cents */
|
||||
totalCost: Scalars['Int'];
|
||||
value: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type EstimatedUsage = {
|
||||
__typename?: 'EstimatedUsage';
|
||||
id: Scalars['ID'];
|
||||
limit: Scalars['Float'];
|
||||
metricType: UsageMetricType;
|
||||
service: EasService;
|
||||
serviceMetric: EasServiceMetric;
|
||||
value: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type ExperimentationQuery = {
|
||||
__typename?: 'ExperimentationQuery';
|
||||
/** Get device experimentation config */
|
||||
@@ -2806,6 +2923,7 @@ export type GitHubRepository = {
|
||||
app: App;
|
||||
githubAppInstallation: GitHubAppInstallation;
|
||||
githubRepositoryIdentifier: Scalars['Int'];
|
||||
githubRepositoryUrl?: Maybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
metadata?: Maybe<GitHubRepositoryMetadata>;
|
||||
nodeIdentifier: Scalars['String'];
|
||||
@@ -3616,8 +3734,9 @@ export type PublishUpdateGroupInput = {
|
||||
gitCommitHash?: InputMaybe<Scalars['String']>;
|
||||
isGitWorkingTreeDirty?: InputMaybe<Scalars['Boolean']>;
|
||||
message?: InputMaybe<Scalars['String']>;
|
||||
rollBackToEmbeddedInfoGroup?: InputMaybe<UpdateRollBackToEmbeddedGroup>;
|
||||
runtimeVersion: Scalars['String'];
|
||||
updateInfoGroup: UpdateInfoGroup;
|
||||
updateInfoGroup?: InputMaybe<UpdateInfoGroup>;
|
||||
};
|
||||
|
||||
export type RescindUserInvitationResult = {
|
||||
@@ -3738,6 +3857,8 @@ export type RootMutation = {
|
||||
build: BuildMutation;
|
||||
/** Mutations that modify an BuildJob */
|
||||
buildJob: BuildJobMutation;
|
||||
/** Mutations for Discord users */
|
||||
discordUser: DiscordUserMutation;
|
||||
/** Mutations that modify an EmailSubscription */
|
||||
emailSubscription: EmailSubscriptionMutation;
|
||||
/** Mutations that create and delete EnvironmentSecrets */
|
||||
@@ -4420,6 +4541,7 @@ export type Update = ActivityTimelineProjectActivity & {
|
||||
group: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
isGitWorkingTreeDirty: Scalars['Boolean'];
|
||||
isRollBackToEmbedded: Scalars['Boolean'];
|
||||
manifestFragment: Scalars['String'];
|
||||
manifestPermalink: Scalars['String'];
|
||||
message?: Maybe<Scalars['String']>;
|
||||
@@ -4575,6 +4697,12 @@ export type UpdateMutationSetCodeSigningInfoArgs = {
|
||||
updateId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type UpdateRollBackToEmbeddedGroup = {
|
||||
android?: InputMaybe<Scalars['Boolean']>;
|
||||
ios?: InputMaybe<Scalars['Boolean']>;
|
||||
web?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type UpdatesFilter = {
|
||||
platform?: InputMaybe<AppPlatform>;
|
||||
runtimeVersions?: InputMaybe<Array<Scalars['String']>>;
|
||||
@@ -4602,8 +4730,8 @@ export type UsageMetricTotal = {
|
||||
__typename?: 'UsageMetricTotal';
|
||||
billingPeriod: BillingPeriod;
|
||||
id: Scalars['ID'];
|
||||
overageMetrics: Array<AccountUsageMetricAndCost>;
|
||||
planMetrics: Array<AccountUsageMetricAndCost>;
|
||||
overageMetrics: Array<EstimatedOverageAndCost>;
|
||||
planMetrics: Array<EstimatedUsage>;
|
||||
/** Total cost of overages, in cents */
|
||||
totalCost: Scalars['Float'];
|
||||
};
|
||||
@@ -4642,6 +4770,8 @@ export type User = Actor & UserActor & {
|
||||
apps: Array<App>;
|
||||
bestContactEmail?: Maybe<Scalars['String']>;
|
||||
created: Scalars['DateTime'];
|
||||
/** Discord account linked to a user */
|
||||
discordUser?: Maybe<DiscordUser>;
|
||||
displayName: Scalars['String'];
|
||||
email?: Maybe<Scalars['String']>;
|
||||
emailVerified: Scalars['Boolean'];
|
||||
@@ -5589,7 +5719,7 @@ export type UpdatePublishMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> } };
|
||||
export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> } };
|
||||
|
||||
export type SetCodeSigningInfoMutationVariables = Exact<{
|
||||
updateId: Scalars['ID'];
|
||||
@@ -5687,7 +5817,7 @@ export type BranchesByAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> }> } } };
|
||||
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> }> } } };
|
||||
|
||||
export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5697,7 +5827,7 @@ export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
|
||||
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
|
||||
|
||||
export type BuildsByIdQueryVariables = Exact<{
|
||||
buildId: Scalars['ID'];
|
||||
@@ -5729,7 +5859,7 @@ export type ViewUpdateChannelOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
|
||||
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
|
||||
|
||||
export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5738,7 +5868,7 @@ export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> }> } } };
|
||||
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> }> } } };
|
||||
|
||||
export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5791,7 +5921,7 @@ export type ViewUpdatesByGroupQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
|
||||
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
|
||||
|
||||
export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5802,7 +5932,7 @@ export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } | null } } };
|
||||
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } | null } } };
|
||||
|
||||
export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -5812,7 +5942,7 @@ export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } } };
|
||||
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> } } };
|
||||
|
||||
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -5848,9 +5978,9 @@ export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id:
|
||||
|
||||
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
|
||||
|
||||
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null };
|
||||
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null };
|
||||
|
||||
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
|
||||
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
|
||||
|
||||
export type WebhookFragment = { __typename?: 'Webhook', id: string, event: WebhookType, url: string, createdAt: any, updatedAt: any };
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const UpdateFragmentNode = gql`
|
||||
runtimeVersion
|
||||
platform
|
||||
manifestFragment
|
||||
isRollBackToEmbedded
|
||||
manifestPermalink
|
||||
gitCommitHash
|
||||
actor {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { instance, mock } from 'ts-mockito';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { defaultPublishPlatforms } from '../../commands/update';
|
||||
import { AssetMetadataStatus } from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
defaultPublishPlatforms,
|
||||
filterExportedPlatformsByFlag,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
getAssetHashFromPath,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { errors, readAndValidateBuildConfigAsync } from '@expo/steps';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
@@ -12,13 +13,33 @@ export async function validateCustomBuildConfigAsync(
|
||||
return;
|
||||
}
|
||||
|
||||
const relativeBuildConfigPath = getCustomBuildConfigPath(profile.config);
|
||||
const buildConfigPath = path.join(projectDir, relativeBuildConfigPath);
|
||||
if (!(await fs.pathExists(buildConfigPath))) {
|
||||
const relativeConfigPath = getCustomBuildConfigPath(profile.config);
|
||||
const configPath = path.join(projectDir, relativeConfigPath);
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
throw new Error(
|
||||
`Custom build configuration file ${chalk.bold(relativeBuildConfigPath)} does not exist.`
|
||||
`Custom build configuration file ${chalk.bold(relativeConfigPath)} does not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await readAndValidateBuildConfigAsync(configPath, { skipNamespacedFunctionsCheck: true });
|
||||
} catch (err) {
|
||||
if (err instanceof errors.BuildConfigYAMLError) {
|
||||
throw new Error(
|
||||
`Custom build configuration file ${chalk.bold(
|
||||
relativeConfigPath
|
||||
)} contains invalid YAML.\n\n${err.message}`
|
||||
);
|
||||
} else if (err instanceof errors.BuildConfigError) {
|
||||
throw new Error(
|
||||
`Custom build configuration file ${chalk.bold(
|
||||
relativeConfigPath
|
||||
)} contains invalid configuration. Please check the docs!\n${err.message}`
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomBuildConfigPath(configFilename: string): string {
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import { ExpoConfig, Platform } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform as EASBuildJobPlatform, Workflow } from '@expo/eas-build-job';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import Joi from 'joi';
|
||||
import mime from 'mime';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import promiseLimit from 'promise-limit';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../branch/utils';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
|
||||
import { PublishMutation } from '../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../graphql/queries/PublishQuery';
|
||||
import Log from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
|
||||
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../update/utils';
|
||||
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
|
||||
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
|
||||
import chunk from '../utils/expodash/chunk';
|
||||
import { truthy } from '../utils/expodash/filter';
|
||||
import uniqBy from '../utils/expodash/uniqBy';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { resolveWorkflowAsync } from './workflow';
|
||||
|
||||
export type ExpoCLIExportPlatformFlag = Platform | 'all';
|
||||
|
||||
@@ -453,3 +467,188 @@ export function isUploadedAssetCountAboveWarningThreshold(
|
||||
const warningThreshold = Math.floor(assetLimitPerUpdateGroup * 0.75);
|
||||
return uploadedAssetCount > warningThreshold;
|
||||
}
|
||||
|
||||
export async function getBranchNameForCommandAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelNameArg,
|
||||
branchNameArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
channelNameArg: string | undefined;
|
||||
branchNameArg: string | undefined;
|
||||
autoFlag: boolean;
|
||||
nonInteractive: boolean;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<string> {
|
||||
if (channelNameArg && branchNameArg) {
|
||||
throw new Error(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
|
||||
);
|
||||
}
|
||||
|
||||
if (channelNameArg) {
|
||||
return await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelNameArg);
|
||||
}
|
||||
|
||||
if (branchNameArg) {
|
||||
return branchNameArg;
|
||||
}
|
||||
|
||||
if (autoFlag) {
|
||||
return await getDefaultBranchNameAsync();
|
||||
} else if (nonInteractive) {
|
||||
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode.');
|
||||
} else {
|
||||
let branchName: string;
|
||||
|
||||
try {
|
||||
const branch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like to use?`,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: `${updateBranch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
|
||||
)}`,
|
||||
}),
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
branchName = branch.name;
|
||||
} catch {
|
||||
// unable to select a branch (network error or no branches for project)
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Provide a branch name:',
|
||||
initial: await getDefaultBranchNameAsync(),
|
||||
validate: value => (value ? true : 'Branch name may not be empty.'),
|
||||
});
|
||||
branchName = name;
|
||||
}
|
||||
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
return branchName;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUpdateMessageForCommandAsync({
|
||||
updateMessageArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
}: {
|
||||
updateMessageArg: string | undefined;
|
||||
autoFlag: boolean;
|
||||
nonInteractive: boolean;
|
||||
jsonFlag: boolean;
|
||||
}): Promise<string> {
|
||||
let updateMessage = updateMessageArg;
|
||||
if (!updateMessageArg && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
const { updateMessageLocal } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessageLocal',
|
||||
message: `Provide an update message:`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
});
|
||||
updateMessage = updateMessageLocal;
|
||||
}
|
||||
|
||||
assert(updateMessage, 'Update message must be specified.');
|
||||
|
||||
const truncatedMessage = truncateUpdateMessage(updateMessage, 1024);
|
||||
if (truncatedMessage !== updateMessage) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
}
|
||||
|
||||
return updateMessage;
|
||||
}
|
||||
|
||||
export const defaultPublishPlatforms: Platform[] = ['android', 'ios'];
|
||||
|
||||
export function getRequestedPlatform(
|
||||
platform: ExpoCLIExportPlatformFlag
|
||||
): RequestedPlatform | null {
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
return RequestedPlatform.Android;
|
||||
case 'ios':
|
||||
return RequestedPlatform.Ios;
|
||||
case 'web':
|
||||
return null;
|
||||
case 'all':
|
||||
return RequestedPlatform.All;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
|
||||
export async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platforms: Platform[],
|
||||
projectDir: string
|
||||
): Promise<{ platform: string; runtimeVersion: string }[]> {
|
||||
for (const platform of platforms) {
|
||||
if (platform === 'web') {
|
||||
continue;
|
||||
}
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged =
|
||||
(await resolveWorkflowAsync(projectDir, platform as EASBuildJobPlatform)) ===
|
||||
Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error(
|
||||
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(platforms)].map(platform => {
|
||||
if (platform === 'web') {
|
||||
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
|
||||
}
|
||||
return {
|
||||
platform,
|
||||
runtimeVersion: nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getRuntimeToPlatformMappingFromRuntimeVersions(
|
||||
runtimeVersions: { platform: string; runtimeVersion: string }[]
|
||||
): { runtimeVersion: string; platforms: string[] }[] {
|
||||
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
|
||||
for (const runtime of runtimeVersions) {
|
||||
const platforms = runtimeVersions
|
||||
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
|
||||
.map(({ platform }) => platform);
|
||||
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
|
||||
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
|
||||
}
|
||||
}
|
||||
return runtimeToPlatformMapping;
|
||||
}
|
||||
@@ -124,6 +124,7 @@ function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
|
||||
runtime: {} as Runtime, // Temporary fix to resolve type errors
|
||||
platform: 'ios',
|
||||
manifestFragment: '...',
|
||||
isRollBackToEmbedded: false,
|
||||
activityTimestamp: '2022-12-07T02:24:43.487Z',
|
||||
branchId: '',
|
||||
awaitingCodeSigningInfo: false,
|
||||
|
||||
@@ -273,7 +273,7 @@ export async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: stri
|
||||
}
|
||||
|
||||
try {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
@@ -343,7 +343,7 @@ export async function ensureEASUpdateIsConfiguredAsync(
|
||||
expWithoutUpdates.sdkVersion
|
||||
);
|
||||
if (!hasExpoUpdates) {
|
||||
await installExpoUpdatesAsync(projectDir, { silent: !Log.isDebug });
|
||||
await installExpoUpdatesAsync(projectDir, { silent: false });
|
||||
Log.withTick('Installed expo updates');
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ export type FormattedUpdateGroupDescription = {
|
||||
group: string;
|
||||
platforms: string;
|
||||
runtimeVersion: string;
|
||||
codeSigningKey: string | undefined;
|
||||
isRollBackToEmbedded: boolean;
|
||||
};
|
||||
|
||||
export type FormattedBranchDescription = {
|
||||
@@ -74,6 +76,8 @@ export function formatUpdateGroup(update: FormattedUpdateGroupDescription): stri
|
||||
{ label: 'Platforms', value: update.platforms },
|
||||
{ label: 'Runtime Version', value: update.runtimeVersion },
|
||||
{ label: 'Message', value: update.message },
|
||||
{ label: 'Code Signing Key', value: update.codeSigningKey ?? 'N/A' },
|
||||
{ label: 'Is Roll Back to Embedded', value: update.isRollBackToEmbedded ? 'Yes' : 'No' },
|
||||
{ label: 'Group ID', value: update.group },
|
||||
]);
|
||||
}
|
||||
@@ -203,6 +207,7 @@ export function getUpdateGroupJsonInfo(updateGroups: UpdateFragment[]): UpdateJs
|
||||
runtimeVersion: update.runtimeVersion,
|
||||
platform: update.platform,
|
||||
manifestPermalink: update.manifestPermalink,
|
||||
isRollBackToEmbedded: update.isRollBackToEmbedded,
|
||||
gitCommitHash: update.gitCommitHash,
|
||||
}));
|
||||
}
|
||||
@@ -213,6 +218,8 @@ export function getUpdateGroupDescriptions(
|
||||
return updateGroups.map(updateGroup => ({
|
||||
message: formatUpdateMessage(updateGroup[0]),
|
||||
runtimeVersion: updateGroup[0].runtimeVersion,
|
||||
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
|
||||
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
|
||||
group: updateGroup[0].group,
|
||||
platforms: formatPlatformForUpdateGroup(updateGroup),
|
||||
}));
|
||||
@@ -225,6 +232,8 @@ export function getUpdateGroupDescriptionsWithBranch(
|
||||
branch: updateGroup[0].branch.name,
|
||||
message: formatUpdateMessage(updateGroup[0]),
|
||||
runtimeVersion: updateGroup[0].runtimeVersion,
|
||||
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
|
||||
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
|
||||
group: updateGroup[0].group,
|
||||
platforms: formatPlatformForUpdateGroup(updateGroup),
|
||||
}));
|
||||
@@ -241,6 +250,8 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra
|
||||
update: {
|
||||
message: formatUpdateMessage(latestUpdate),
|
||||
runtimeVersion: latestUpdate.runtimeVersion,
|
||||
isRollBackToEmbedded: latestUpdate.isRollBackToEmbedded,
|
||||
codeSigningKey: latestUpdate.codeSigningInfo?.keyid,
|
||||
group: latestUpdate.group,
|
||||
platforms: getPlatformsForGroup({
|
||||
group: latestUpdate.group,
|
||||
|
||||
@@ -11,6 +11,7 @@ jest.mock('@expo/eas-json', () => {
|
||||
const EasJsonUtilsMock = {
|
||||
getBuildProfileAsync: jest.fn(),
|
||||
getBuildProfileNamesAsync: jest.fn(),
|
||||
getBuildProfileDeprecationWarnings: jest.fn(() => []),
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
@@ -29,7 +30,7 @@ describe(getProfilesAsync, () => {
|
||||
});
|
||||
|
||||
it('defaults to production profile', async () => {
|
||||
const easJsonAccessor = new EasJsonAccessor('/fake');
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath('/fake');
|
||||
const result = await getProfilesAsync({
|
||||
easJsonAccessor,
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
@@ -51,7 +52,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
easJsonAccessor: new EasJsonAccessor('/fake'),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath('/fake'),
|
||||
platforms: [Platform.ANDROID],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
@@ -60,7 +61,7 @@ describe(getProfilesAsync, () => {
|
||||
});
|
||||
|
||||
it('gets a specific profile', async () => {
|
||||
const easJsonAccessor = new EasJsonAccessor('/fake');
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath('/fake');
|
||||
const result = await getProfilesAsync({
|
||||
easJsonAccessor,
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
@@ -85,7 +86,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
easJsonAccessor: new EasJsonAccessor('/fake'),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath('/fake'),
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
|
||||
@@ -16,8 +16,9 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import { Response } from '../fetch';
|
||||
import { PartialManifest, PartialManifestAsset } from '../graphql/generated';
|
||||
import areSetsEqual from './expodash/areSetsEqual';
|
||||
|
||||
type CodeSigningInfo = {
|
||||
export type CodeSigningInfo = {
|
||||
privateKey: PKI.rsa.PrivateKey;
|
||||
certificate: PKI.Certificate;
|
||||
codeSigningMetadata: { alg: string; keyid: string };
|
||||
@@ -101,7 +102,7 @@ export async function getKeyAndCertificateFromPathsAsync({
|
||||
};
|
||||
}
|
||||
|
||||
export async function getManifestBodyAsync(res: Response): Promise<string | null> {
|
||||
async function getMultipartBodyPartAsync(res: Response, partName: string): Promise<string | null> {
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (!contentType) {
|
||||
throw new Error('The multipart manifest response is missing the content-type header');
|
||||
@@ -111,11 +112,19 @@ export async function getManifestBodyAsync(res: Response): Promise<string | null
|
||||
contentType,
|
||||
Buffer.from(bodyBuffer)
|
||||
);
|
||||
const manifestPart = multipartParts.find(part => isMultipartPartWithName(part, 'manifest'));
|
||||
const manifestPart = multipartParts.find(part => isMultipartPartWithName(part, partName));
|
||||
return manifestPart?.body ?? null;
|
||||
}
|
||||
|
||||
export function signManifestBody(body: string, codeSigningInfo: CodeSigningInfo): string {
|
||||
export async function getManifestBodyAsync(res: Response): Promise<string | null> {
|
||||
return await getMultipartBodyPartAsync(res, 'manifest');
|
||||
}
|
||||
|
||||
export async function getDirectiveBodyAsync(res: Response): Promise<string | null> {
|
||||
return await getMultipartBodyPartAsync(res, 'directive');
|
||||
}
|
||||
|
||||
export function signBody(body: string, codeSigningInfo: CodeSigningInfo): string {
|
||||
return signBufferRSASHA256AndVerify(
|
||||
codeSigningInfo.privateKey,
|
||||
codeSigningInfo.certificate,
|
||||
@@ -181,3 +190,20 @@ export function checkManifestBodyAgainstUpdateInfoGroup(
|
||||
assertAssetParity(correspondingManifestResponseBodyAssetJSON, nullthrows(partialManifestAsset));
|
||||
}
|
||||
}
|
||||
|
||||
export function checkDirectiveBodyAgainstUpdateInfoGroup(directiveResponseBody: string): void {
|
||||
const directiveResponseBodyJSON = JSON.parse(directiveResponseBody);
|
||||
|
||||
if (
|
||||
!areSetsEqual(
|
||||
new Set(Object.keys(directiveResponseBodyJSON)),
|
||||
new Set(['extra', 'type', 'parameters'])
|
||||
)
|
||||
) {
|
||||
throw new Error('Code signing directive integrity error: Unexpected keys');
|
||||
}
|
||||
|
||||
if (directiveResponseBodyJSON.type !== 'rollBackToEmbedded') {
|
||||
throw new Error('Code signing directive integrity error: Incorrect directive type');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config-types';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import { boolish } from 'getenv';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import resolveFrom, { silent as silentResolveFrom } from 'resolve-from';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../log';
|
||||
@@ -63,7 +63,8 @@ export async function expoCommandAsync(
|
||||
args: string[],
|
||||
{ silent = false }: { silent?: boolean } = {}
|
||||
): Promise<void> {
|
||||
const expoCliPath = resolveFrom(projectDir, 'expo/bin/cli.js');
|
||||
const expoCliPath =
|
||||
silentResolveFrom(projectDir, 'expo/bin/cli') ?? resolveFrom(projectDir, 'expo/bin/cli.js');
|
||||
const spawnPromise = spawnAsync(expoCliPath, args, {
|
||||
stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import areSetsEqual from '../areSetsEqual';
|
||||
|
||||
describe(areSetsEqual, () => {
|
||||
it.each([
|
||||
[new Set([1, 2]), new Set([1, 2])],
|
||||
[new Set([1, 2]), new Set([2, 1])],
|
||||
])('equal cases: %p', (a, b) => {
|
||||
expect(areSetsEqual(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[new Set([1, 2, 3]), new Set([1, 2])],
|
||||
[new Set([1, 2]), new Set([1, 2, 3])],
|
||||
])('non-equal cases: %p', (a, b) => {
|
||||
expect(areSetsEqual(a, b)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function areSetsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
||||
return a.size === b.size && [...a].every(value => b.has(value));
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
SubmitProfile,
|
||||
} from '@expo/eas-json';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
|
||||
type EasProfile<T extends ProfileType> = T extends 'build'
|
||||
? BuildProfile<Platform>
|
||||
: SubmitProfile<Platform>;
|
||||
@@ -57,11 +59,15 @@ async function readProfileAsync<T extends ProfileType>({
|
||||
profileName?: string;
|
||||
}): Promise<EasProfile<T>> {
|
||||
if (type === 'build') {
|
||||
return (await EasJsonUtils.getBuildProfileAsync(
|
||||
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
easJsonAccessor,
|
||||
platform,
|
||||
profileName
|
||||
)) as EasProfile<T>;
|
||||
);
|
||||
|
||||
maybePrintBuildProfileDeprecationWarnings(buildProfile, profileName);
|
||||
|
||||
return buildProfile as EasProfile<T>;
|
||||
} else {
|
||||
return (await EasJsonUtils.getSubmitProfileAsync(
|
||||
easJsonAccessor,
|
||||
@@ -70,3 +76,32 @@ async function readProfileAsync<T extends ProfileType>({
|
||||
)) as EasProfile<T>;
|
||||
}
|
||||
}
|
||||
|
||||
let hasPrintedDeprecationWarnings = false;
|
||||
|
||||
function maybePrintBuildProfileDeprecationWarnings(
|
||||
buildProfile: BuildProfile<Platform>,
|
||||
profileName?: string
|
||||
): void {
|
||||
if (hasPrintedDeprecationWarnings) {
|
||||
return;
|
||||
}
|
||||
const deprecationWarnings = EasJsonUtils.getBuildProfileDeprecationWarnings(
|
||||
buildProfile,
|
||||
profileName
|
||||
);
|
||||
if (deprecationWarnings.length === 0) {
|
||||
return;
|
||||
}
|
||||
Log.newLine();
|
||||
Log.warn('Detected deprecated fields in eas.json:');
|
||||
for (const warning of deprecationWarnings) {
|
||||
const warnlog: string = warning.message.map(line => `\t${line}`).join('\n');
|
||||
Log.warn(warnlog);
|
||||
if (warning.docsUrl) {
|
||||
Log.warn(`\t${learnMore(warning.docsUrl)}`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
hasPrintedDeprecationWarnings = true;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "3.8.0",
|
||||
"version": "3.9.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.18.6",
|
||||
"@expo/eas-build-job": "0.2.106",
|
||||
"@expo/eas-build-job": "1.0.7",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
|
||||
@@ -74,11 +74,6 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"cacheDefaultPaths": {
|
||||
"type": "boolean",
|
||||
"description": "Specifies whether to cache the recommended set of files, currently only Podfile.lock is cached by default for iOS build and nothing is cached for Android.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EasJsonAccessor patching JSON file 1`] = `
|
||||
exports[`fromProjectPath patching JSON file 1`] = `
|
||||
"{
|
||||
"build": {
|
||||
"production": {
|
||||
@@ -14,7 +14,7 @@ exports[`EasJsonAccessor patching JSON file 1`] = `
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`EasJsonAccessor patching JSON5 file (preserves comments) 1`] = `
|
||||
exports[`fromProjectPath patching JSON5 file (preserves comments) 1`] = `
|
||||
"{
|
||||
"build": {
|
||||
"production": {
|
||||
|
||||
@@ -15,7 +15,7 @@ beforeEach(async () => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(EasJsonAccessor, () => {
|
||||
describe(EasJsonAccessor.fromProjectPath, () => {
|
||||
test('patching JSON file', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
@@ -24,7 +24,7 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await accessor.readAsync();
|
||||
accessor.patch(o => {
|
||||
o.build.production.env.ABC = '456';
|
||||
@@ -44,7 +44,7 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await accessor.readAsync();
|
||||
accessor.patch(o => {
|
||||
o.build.production.env.ABC = '456';
|
||||
@@ -67,7 +67,7 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(
|
||||
/^Found invalid character in .*eas\.json.+/
|
||||
@@ -82,7 +82,57 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(/^.*eas\.json.* is empty\.$/g);
|
||||
});
|
||||
});
|
||||
|
||||
describe(EasJsonAccessor.fromRawString, () => {
|
||||
test('patching JSON file', async () => {
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
await fsReal.readFile(path.join(fixturesDir, 'eas-json.json'), 'utf-8')
|
||||
);
|
||||
await accessor.readAsync();
|
||||
expect(() =>
|
||||
accessor.patch(o => {
|
||||
o.build.production.env.ABC = '456';
|
||||
return o;
|
||||
})
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('reading invalid JSON5 object', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./eas.json': await fsReal.readFile(
|
||||
path.join(fixturesDir, 'eas-invalid-json5.json'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
await fsReal.readFile(path.join(fixturesDir, 'eas-invalid-json5.json'), 'utf-8')
|
||||
);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(
|
||||
/^Found invalid character in .*eas\.json.+/
|
||||
);
|
||||
});
|
||||
|
||||
test('reading empty JSON file', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./eas.json': await fsReal.readFile(path.join(fixturesDir, 'eas-empty.json'), 'utf-8'),
|
||||
},
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
await fsReal.readFile(path.join(fixturesDir, 'eas-empty.json'), 'utf-8')
|
||||
);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(/^.*eas\.json.* is empty\.$/g);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,33 @@ test('minimal valid eas.json for both platforms', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'production'
|
||||
);
|
||||
|
||||
expect(androidProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
});
|
||||
|
||||
test('minimal valid eas.json for both platforms when reading eas.json from string', async () => {
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
JSON.stringify({
|
||||
build: {
|
||||
production: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -52,7 +78,7 @@ test('valid eas.json for development client builds', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'debug');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -82,7 +108,7 @@ test(`valid eas.json with autoIncrement flag at build profile root`, async () =>
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -111,7 +137,7 @@ test('valid profile for internal distribution on Android', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'internal');
|
||||
expect(profile).toEqual({
|
||||
distribution: 'internal',
|
||||
@@ -133,7 +159,7 @@ test('valid profile extending other profile', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
|
||||
const extendedProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -176,7 +202,7 @@ test('valid profile extending other profile with platform specific envs', async
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -230,7 +256,6 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
},
|
||||
android: {
|
||||
cache: {
|
||||
cacheDefaultPaths: false,
|
||||
customPaths: ['somefakepath'],
|
||||
},
|
||||
},
|
||||
@@ -238,7 +263,7 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -261,7 +286,6 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
cache: {
|
||||
cacheDefaultPaths: false,
|
||||
customPaths: ['somefakepath'],
|
||||
},
|
||||
});
|
||||
@@ -282,7 +306,7 @@ test('valid eas.json with missing profile', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'debug');
|
||||
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: debug');
|
||||
});
|
||||
@@ -294,7 +318,7 @@ test('invalid eas.json when using wrong buildType', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(promise).rejects.toThrowError(
|
||||
@@ -305,7 +329,7 @@ test('invalid eas.json when using wrong buildType', async () => {
|
||||
test('empty json', async () => {
|
||||
await fs.writeJson('/project/eas.json', {});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: production');
|
||||
});
|
||||
@@ -317,7 +341,7 @@ test('invalid semver value', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(promise).rejects.toThrowError(
|
||||
@@ -332,7 +356,7 @@ test('invalid release channel', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
|
||||
});
|
||||
@@ -345,7 +369,7 @@ test('get profile names', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const allProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(accessor);
|
||||
expect(allProfileNames.sort()).toEqual(['blah', 'production'].sort());
|
||||
});
|
||||
@@ -359,7 +383,7 @@ test('invalid resourceClass at build profile root', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
@@ -377,7 +401,7 @@ test('iOS-specific resourceClass', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
).resolves.not.toThrow();
|
||||
@@ -394,7 +418,7 @@ test('Android-specific resourceClass', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production')
|
||||
).resolves.not.toThrow();
|
||||
@@ -414,7 +438,7 @@ test('build profile with platform-specific custom build config', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
@@ -442,7 +466,7 @@ test('build profiles with both platform build config', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
|
||||
@@ -19,7 +19,7 @@ test('minimal allowed eas.json for both platforms', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
@@ -50,7 +50,7 @@ test('android config with all required values', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
@@ -80,7 +80,7 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
|
||||
|
||||
try {
|
||||
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
@@ -114,7 +114,7 @@ test('ios config with all required values', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
@@ -148,7 +148,7 @@ test('ios config with ascApiKey fields set to env var', async () => {
|
||||
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
|
||||
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
|
||||
process.env.ASC_API_KEY_ID = 'ABCD';
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
@@ -189,7 +189,7 @@ test('valid profile extending other profile', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'base');
|
||||
const extendedProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
@@ -233,7 +233,7 @@ test('get profile names', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const allProfileNames = await EasJsonUtils.getSubmitProfileNamesAsync(accessor);
|
||||
expect(allProfileNames.sort()).toEqual(['production', 'blah'].sort());
|
||||
});
|
||||
@@ -38,7 +38,7 @@ const customErrorMessageHandlers: ((err: ValidationError) => void)[] = [
|
||||
];
|
||||
|
||||
export class EasJsonAccessor {
|
||||
private easJsonPath: string;
|
||||
private easJsonPath: string | undefined;
|
||||
|
||||
private isJson5 = false;
|
||||
private easJson: EasJson | undefined;
|
||||
@@ -46,8 +46,25 @@ export class EasJsonAccessor {
|
||||
private easJsonRawObject: any | undefined;
|
||||
private easJsonPatched: boolean = false;
|
||||
|
||||
constructor(projectDir: string) {
|
||||
this.easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
|
||||
private constructor({ projectDir }: { projectDir: string });
|
||||
private constructor({ easJsonRawContents }: { easJsonRawContents: string });
|
||||
private constructor({
|
||||
projectDir,
|
||||
easJsonRawContents,
|
||||
}: {
|
||||
projectDir?: string;
|
||||
easJsonRawContents?: string;
|
||||
}) {
|
||||
this.easJsonPath = projectDir && EasJsonAccessor.formatEasJsonPath(projectDir);
|
||||
this.easJsonRawContents = easJsonRawContents;
|
||||
}
|
||||
|
||||
public static fromProjectPath(projectDir: string): EasJsonAccessor {
|
||||
return new EasJsonAccessor({ projectDir });
|
||||
}
|
||||
|
||||
public static fromRawString(easJsonRawContents: string): EasJsonAccessor {
|
||||
return new EasJsonAccessor({ easJsonRawContents });
|
||||
}
|
||||
|
||||
public static formatEasJsonPath(projectDir: string): string {
|
||||
@@ -81,6 +98,9 @@ export class EasJsonAccessor {
|
||||
}
|
||||
|
||||
public async writeAsync(): Promise<void> {
|
||||
if (!this.easJsonPath) {
|
||||
throw new Error('Updates are not supported for EasJsonAccessor created from string.');
|
||||
}
|
||||
if (!this.easJsonPatched) {
|
||||
return;
|
||||
}
|
||||
@@ -89,6 +109,9 @@ export class EasJsonAccessor {
|
||||
}
|
||||
|
||||
public patch(fn: (easJsonRawObject: any) => any): void {
|
||||
if (!this.easJsonPath) {
|
||||
throw new Error('Updates are not supported for EasJsonAccessor created from string.');
|
||||
}
|
||||
assert(
|
||||
this.easJsonRawContents && this.easJsonRawObject,
|
||||
'call readAsync/readRawJsonAsync first'
|
||||
@@ -104,16 +127,24 @@ export class EasJsonAccessor {
|
||||
}
|
||||
|
||||
public async readRawJsonAsync(): Promise<any> {
|
||||
if (!(await fs.pathExists(this.easJsonPath))) {
|
||||
throw new MissingEasJsonError(
|
||||
`${chalk.bold('eas.json')} could not be found at ${
|
||||
this.easJsonPath
|
||||
}. Learn more at https://expo.fyi/eas-json`
|
||||
);
|
||||
if (this.easJsonPath) {
|
||||
if (!(await fs.pathExists(this.easJsonPath))) {
|
||||
throw new MissingEasJsonError(
|
||||
`${chalk.bold('eas.json')} could not be found at ${
|
||||
this.easJsonPath
|
||||
}. Learn more at https://expo.fyi/eas-json`
|
||||
);
|
||||
}
|
||||
this.easJsonRawContents = await fs.readFile(this.easJsonPath, 'utf-8');
|
||||
}
|
||||
return this.parseRawJson();
|
||||
}
|
||||
|
||||
this.easJsonRawContents = await fs.readFile(this.easJsonPath, 'utf-8');
|
||||
|
||||
private parseRawJson(): any {
|
||||
assert(
|
||||
this.easJsonRawContents !== undefined,
|
||||
'easJsonRawContents needs to be set before calling parseRawJson'
|
||||
);
|
||||
if (this.easJsonRawContents.trim().length === 0) {
|
||||
throw new InvalidEasJsonError(`${chalk.bold('eas.json')} is empty.`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { AndroidReleaseStatus, AndroidReleaseTrack, SubmitProfile } from './submit/types';
|
||||
export { getDefaultProfile as getDefaultSubmitProfile } from './submit/resolver';
|
||||
export { EasJson, ProfileType, AppVersionSource } from './types';
|
||||
export { Platform } from '@expo/eas-build-job';
|
||||
export {
|
||||
AndroidVersionAutoIncrement,
|
||||
BuildProfile,
|
||||
|
||||
@@ -8,6 +8,11 @@ import { resolveSubmitProfile } from './submit/resolver';
|
||||
import { SubmitProfile } from './submit/types';
|
||||
import { EasJson } from './types';
|
||||
|
||||
interface EasJsonDeprecationWarning {
|
||||
message: string[];
|
||||
docsUrl?: string;
|
||||
}
|
||||
|
||||
export class EasJsonUtils {
|
||||
public static async getBuildProfileNamesAsync(accessor: EasJsonAccessor): Promise<string[]> {
|
||||
const easJson = await accessor.readAsync();
|
||||
@@ -23,6 +28,26 @@ export class EasJsonUtils {
|
||||
return resolveBuildProfile({ easJson, platform, profileName });
|
||||
}
|
||||
|
||||
public static getBuildProfileDeprecationWarnings(
|
||||
buildProfile: BuildProfile,
|
||||
profileName?: string
|
||||
): EasJsonDeprecationWarning[] {
|
||||
const warnings: EasJsonDeprecationWarning[] = [];
|
||||
|
||||
if (buildProfile.cache?.cacheDefaultPaths !== undefined) {
|
||||
warnings.push({
|
||||
message: [
|
||||
`The "build.${
|
||||
profileName ?? 'production'
|
||||
}.cache.cacheDefaultPaths" field in eas.json is deprecated and will be removed in the future.`,
|
||||
],
|
||||
docsUrl: 'https://docs.expo.dev/build-reference/caching/#ios-dependencies',
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public static async getCliConfigAsync(accessor: EasJsonAccessor): Promise<EasJson['cli'] | null> {
|
||||
try {
|
||||
const easJson = await accessor.readAsync();
|
||||
|
||||
@@ -5,9 +5,11 @@ set -eo pipefail
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
|
||||
LOCAL_TS_PATH="$ROOT_DIR/packages/eas-cli/src/build/local.ts"
|
||||
|
||||
LOCAL_BUILD_PLUGIN_NPM_TAG="eas-cli"
|
||||
|
||||
echo "Checking if eas-cli-local-build-plugin upgrade is available"
|
||||
|
||||
plugin_version_latest=$(npm info eas-cli-local-build-plugin@latest version)
|
||||
plugin_version_latest=$(npm info eas-cli-local-build-plugin@$LOCAL_BUILD_PLUGIN_NPM_TAG version)
|
||||
plugin_version_current=$(grep 'const PLUGIN_PACKAGE_VERSION' $LOCAL_TS_PATH | cut -d"'" -f2)
|
||||
|
||||
if [[ "$plugin_version_latest" == "$plugin_version_current" ]]; then
|
||||
|
||||
@@ -1324,10 +1324,10 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@expo/apple-utils@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-1.1.0.tgz#62cd724436b8eaa9cafcc3648ea211af8409fc71"
|
||||
integrity sha512-caw0o9HeQHX7xeXppkXI3B/PmPyVOkEjWgXdv0dYzgW/rwvZRLwtMNFXX5t+dJy2hd2UpZlrixaoYyvxuXPtbQ==
|
||||
"@expo/apple-utils@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-1.2.0.tgz#227d118089a4645b076162f3b513dfc68763ca21"
|
||||
integrity sha512-MotvQqu8CNVlnl9nsISobXDjEzSMfT7U5Zk+1rhOFoJhaZhlEkARxumUFUgH6cYnK7J2mMd3YxM+G769arkEKQ==
|
||||
|
||||
"@expo/bunyan@^4.0.0":
|
||||
version "4.0.0"
|
||||
@@ -1390,10 +1390,10 @@
|
||||
slugify "^1.3.4"
|
||||
sucrase "^3.20.0"
|
||||
|
||||
"@expo/eas-build-job@0.2.106":
|
||||
version "0.2.106"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.106.tgz#a10f791986bfa9777a13b2b0e9c820673e9e38ba"
|
||||
integrity sha512-V7ihSFNFaATM31P1b/tGrGG7sjuXBT5ZAzKnybTWr5Xl1ODOdEpSC1c9bzs9SUr2iWUFVsxyd2/7+f2+7r/l4A==
|
||||
"@expo/eas-build-job@1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.7.tgz#4c50dbc37faf000c5a5d791e69995bbf0eda39ff"
|
||||
integrity sha512-LBYXWS/wp6YnyNDUqt+5+k0J7P6gf3/hjf48yJp+BcLabQbqWqAuz7eqPMEDt9bezMX2fEAiYywSBdnRJKo3Ow==
|
||||
dependencies:
|
||||
joi "^17.7.0"
|
||||
semver "^7.3.8"
|
||||
@@ -1433,6 +1433,13 @@
|
||||
json5 "^2.2.2"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/logger/-/logger-1.0.0.tgz#41f17209e11aa83eabd5722c0efbc5b80efc7c41"
|
||||
integrity sha512-i07o2xUsmtpC2mQ3ZHOJRtFEqWTfQabOb3U5Q0Hwip/BvI0yj8Pweu2ExhKLERbGugt4tSSLdQJUcKQbOrue6g==
|
||||
dependencies:
|
||||
"@expo/bunyan" "^4.0.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/multipart-body-parser/-/multipart-body-parser-1.1.0.tgz#ba402b3047cf14ef17b4d785ec030dd6474ae6e4"
|
||||
@@ -1565,6 +1572,26 @@
|
||||
dependencies:
|
||||
cross-spawn "^7.0.3"
|
||||
|
||||
"@expo/spawn-async@^1.7.0":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58"
|
||||
integrity sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.3"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@expo/steps/-/steps-1.0.8.tgz#06fe9ca5bde449255661cf090666e70dd2ae5345"
|
||||
integrity sha512-1SxHOtfLwCdKxOUtbXjz548JQlkkVoNjaydNmo7zZgPvD/P4aviaOSCL3StUpQuWNlcERLzjOn8/NgvsdDmMyw==
|
||||
dependencies:
|
||||
"@expo/logger" "1.0.0"
|
||||
"@expo/spawn-async" "^1.7.0"
|
||||
arg "^5.0.2"
|
||||
joi "^17.7.0"
|
||||
this-file "^2.0.3"
|
||||
uuid "^9.0.0"
|
||||
yaml "^2.2.1"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/timeago.js/-/timeago.js-1.0.0.tgz#8fb2b17e93e7a8d28387a4d292af12e071040045"
|
||||
@@ -3984,12 +4011,7 @@
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@sideway/formula@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||
|
||||
"@sideway/formula@^3.0.1":
|
||||
"@sideway/formula@^3.0.0", "@sideway/formula@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
||||
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
||||
@@ -4837,6 +4859,11 @@ arg@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
arg@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
|
||||
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
@@ -12814,6 +12841,11 @@ thenify-all@^1.0.0:
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
this-file@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/this-file/-/this-file-2.0.3.tgz#13bd2bcfbab2ce86a37a15689df1d14861032b1b"
|
||||
integrity sha512-IdMH1bUkVJdJjM7o8v83Mv4QvVPdkAofur20STl2Bbw9uMuuS/bT/PZURkEdZsy9XC/1ZXWgZ1wIL9nvouGaEg==
|
||||
|
||||
through2@^2.0.0:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
@@ -13327,7 +13359,7 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
|
||||
integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
|
||||
|
||||
[email protected]:
|
||||
[email protected], uuid@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||
@@ -13767,6 +13799,11 @@ yaml@^1.10.0, yaml@^1.7.2:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
|
||||
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
|
||||
|
||||
yaml@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4"
|
||||
integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==
|
||||
|
||||
[email protected], yargs-parser@^20.2.2:
|
||||
version "20.2.4"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
|
||||
|
||||
Reference in new issue
Block a user