mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ffb07402 | ||
|
|
0b2b6ddc14 | ||
|
|
0cff5ef83f | ||
|
|
396c45269e | ||
|
|
98ad9798c5 | ||
|
|
b4741293ba | ||
|
|
43a6aa826b | ||
|
|
e0a9384e56 | ||
|
|
b5ca8c63d9 | ||
|
|
c98401131a | ||
|
|
c1cd0f584f | ||
|
|
f82aae7fc9 | ||
|
|
c349ad7a40 | ||
|
|
3500a4d5ec | ||
|
|
149d07b28e | ||
|
|
e248bf3fc3 | ||
|
|
f07b692ead | ||
|
|
ebfe69535f | ||
|
|
58dbee8dee | ||
|
|
feca0dc721 | ||
|
|
81831d156d | ||
|
|
55c859cea7 | ||
|
|
613748b2f0 | ||
|
|
bbd4bca83d | ||
|
|
007866f278 | ||
|
|
1d2e9818ed | ||
|
|
34deafe440 | ||
|
|
55d8b650d3 | ||
|
|
3ef6802d55 | ||
|
|
e9484ffa5a | ||
|
|
c2b50a3a08 | ||
|
|
06bb202bdf | ||
|
|
64b67e9ea7 | ||
|
|
faf1e90bed |
No files matched your search
@@ -8,14 +8,67 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add 2022 Apple capabilities. ([#1307](https://github.com/expo/eas-cli/pull/1307) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Print warning when eas-cli is installed as project dependency. ([#1310](https://github.com/expo/eas-cli/pull/1310) by [@dsokal](https://github.com/dsokal))
|
||||
- Add eas.json to skip push notifications credentials setup. ([#1315](https://github.com/expo/eas-cli/pull/1315) by [@dsokal](https://github.com/dsokal))
|
||||
- Warn about EAS outages when running `build`, `submit`, and `update` commands. ([#1312](https://github.com/expo/eas-cli/pull/1312) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Replace promptToCreateProjectIfNotExistsAsync with getProjectIdAsync. ([#1303](https://github.com/expo/eas-cli/pull/1303) by [@wschurman](https://github.com/wschurman))
|
||||
- Use AppQuery instead of ProjectQuery. ([#1304](https://github.com/expo/eas-cli/pull/1304) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Improve instructions on setting `extra.eas.projectId` in app configuration. ([#1316](https://github.com/expo/eas-cli/pull/1316) by [@dsokal](https://github.com/dsokal))
|
||||
- Improve copy in EAS Submit - "e-mail" -> email, and make the link to app on App Store go directly to the TestFlight tab. ([#1318](https://github.com/expo/eas-cli/pull/1318) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [1.1.1](https://github.com/expo/eas-cli/releases/tag/v1.1.1) - 2022-08-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for `autoIncrement` option at the root of the build profile. ([#1298](https://github.com/expo/eas-cli/pull/1298) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Handle trailing backslash in `.gitignore`. ([#1306](https://github.com/expo/eas-cli/pull/1306) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Retry ASC Api Key downloads if it has not fully propagated on Apple's infrastructure. ([#1302](https://github.com/expo/eas-cli/pull/1302) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [1.1.0](https://github.com/expo/eas-cli/releases/tag/v1.1.0) - 2022-08-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for passing platform flag to `credentials` command. ([#1277](https://github.com/expo/eas-cli/pull/1277) by [@Simek](https://github.com/Simek))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Prevent throwing dynamic app config write error when configuring project ID. ([#1301](https://github.com/expo/eas-cli/pull/1301) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove unused dependency and devDependency from the CLI. ([#1297](https://github.com/expo/eas-cli/pull/1297) by [@Simek](https://github.com/Simek))
|
||||
|
||||
## [1.0.0](https://github.com/expo/eas-cli/releases/tag/v1.0.0) - 2022-08-22
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Remove timeout when waiting for build / submission. ([#1289](https://github.com/expo/eas-cli/pull/1289) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add metadata support for dynamic store.config.js files. ([#1270](https://github.com/expo/eas-cli/pull/1270) by [@byCedric](https://github.com/byCedric))
|
||||
- Improve reliability of update asset presigned upload requests. ([#1278](https://github.com/expo/eas-cli/pull/1278) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Rebind `console.info` correctly after `ora` instance stops. ([#1113](https://github.com/expo/eas-cli/pull/1113) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Fix initializing git repository in monorepo. ([#1279](https://github.com/expo/eas-cli/pull/1279) by [@dsokal](https://github.com/dsokal))
|
||||
- Limit the number of SignedAssetUploadSpecifications fetched at a time. ([#1287](https://github.com/expo/eas-cli/pull/1287) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove unused install script. ([#1280](https://github.com/expo/eas-cli/pull/1280), [#1281](https://github.com/expo/eas-cli/pull/1281) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Remove "please" from output ([#1250](https://github.com/expo/eas-cli/pull/1250) by [@jonsamp](https://github.com/jonsamp))
|
||||
|
||||
## [0.60.0](https://github.com/expo/eas-cli/releases/tag/v0.60.0) - 2022-08-12
|
||||
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
packages/eas-cli/src/build @dsokal @wkozyra95
|
||||
packages/eas-cli/src/commands/build @dsokal @wkozyra95
|
||||
|
||||
packages/eas-cli/src/submit @barthap @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
|
||||
packages/eas-cli/src/submit @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @dsokal
|
||||
|
||||
packages/eas-cli/src/update @kgc00 @wschurman
|
||||
packages/eas-cli/src/commands/update @kgc00 @wschurman
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.60.0"
|
||||
"version": "1.2.0"
|
||||
}
|
||||
+72
-47
@@ -19,6 +19,28 @@ npm install -g eas-cli
|
||||
yarn global add eas-cli
|
||||
```
|
||||
|
||||
## Enforcing eas-cli version for your project
|
||||
|
||||
If you want to enforce the `eas-cli` version for your project, use the `"cli.version"` field in **eas.json**. Installing `eas-cli` to your project dependencies is strongly discouraged because it may cause dependency conflicts that are difficult to debug.
|
||||
|
||||
An example of **eas.json** that enforces `eas-cli` in version `1.0.0` or newer:
|
||||
|
||||
```json
|
||||
{
|
||||
"cli": {
|
||||
"version": ">=1.0.0"
|
||||
},
|
||||
"build": {
|
||||
// build profiles
|
||||
}
|
||||
"submit": {
|
||||
// submit profiles
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Learn more: https://docs.expo.dev/build-reference/eas-json/
|
||||
|
||||
# Usage
|
||||
|
||||
```sh
|
||||
@@ -102,7 +124,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -119,7 +141,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -136,7 +158,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -150,7 +172,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -199,7 +221,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -219,7 +241,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -240,7 +262,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -259,7 +281,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -283,7 +305,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -313,7 +335,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -327,7 +349,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -344,7 +366,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -379,7 +401,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/v0.60.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -412,7 +434,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -459,7 +481,7 @@ DESCRIPTION
|
||||
Update version of an app.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v0.60.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/v1.2.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -478,7 +500,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/v0.60.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/v1.2.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -495,7 +517,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -515,7 +537,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -536,7 +558,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -553,7 +575,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -573,7 +595,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -591,7 +613,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -599,13 +621,16 @@ manage credentials
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas credentials
|
||||
$ eas credentials [-p android|ios]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios)
|
||||
|
||||
DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -619,7 +644,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/v0.60.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -637,7 +662,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -654,7 +679,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -668,7 +693,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -682,7 +707,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -764,7 +789,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/v0.60.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -781,7 +806,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/v0.60.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -795,7 +820,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -812,7 +837,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -832,7 +857,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/v0.60.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -849,7 +874,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -863,7 +888,7 @@ DESCRIPTION
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -893,7 +918,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -923,7 +948,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -940,7 +965,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -960,7 +985,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -979,7 +1004,7 @@ DESCRIPTION
|
||||
view the recent updates for a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -999,7 +1024,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -1019,7 +1044,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -1036,7 +1061,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -1053,7 +1078,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -1074,7 +1099,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1091,7 +1116,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.60.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v1.2.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
|
||||
Generated
+1031
-48
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.60.0",
|
||||
"version": "1.2.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.33",
|
||||
"@expo/apple-utils": "0.0.0-alpha.34",
|
||||
"@expo/code-signing-certificates": "0.0.2",
|
||||
"@expo/config": "7.0.1",
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.85",
|
||||
"@expo/eas-json": "0.60.0",
|
||||
"@expo/eas-json": "1.2.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/package-manager": "0.0.56",
|
||||
@@ -63,12 +63,12 @@
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-retry": "2.0.1",
|
||||
"prompts": "2.4.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.7",
|
||||
"slash": "3.0.0",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.0",
|
||||
@@ -90,6 +90,7 @@
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.0.4",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.0.14",
|
||||
"@types/semver": "7.3.10",
|
||||
"@types/tar": "6.1.2",
|
||||
@@ -98,12 +99,10 @@
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.27.2",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"memfs": "3.4.7",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.9",
|
||||
"rimraf": "3.0.2",
|
||||
"tempy": "0.7.0",
|
||||
"ts-deepmerge": "2.0.5",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.1",
|
||||
|
||||
@@ -15,6 +15,9 @@ export enum SubmissionEvent {
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
||||
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
||||
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
|
||||
@@ -47,7 +47,7 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error('Please update eas.json and come back again.');
|
||||
Log.error('Update eas.json and come back again.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,18 +134,18 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
|
||||
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
Log.error('EAS Build API has changed. Upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
'EAS Build is down for maintenance. Try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
|
||||
Log.error(
|
||||
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
'EAS Build free tier is temporarily disabled. Try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
@@ -157,7 +157,7 @@ function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
@@ -243,11 +243,7 @@ type MaybeBuildFragment = BuildFragment | null;
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
|
||||
{
|
||||
// 2 hours (max build time limit) + 10 minutes (possible queue time)
|
||||
timeoutSec = 2 * 60 * 60 + 10 * 60,
|
||||
intervalSec = 10,
|
||||
} = {}
|
||||
{ intervalSec = 10 } = {}
|
||||
): Promise<MaybeBuildFragment[]> {
|
||||
let spinner;
|
||||
let originalSpinnerText;
|
||||
@@ -259,8 +255,7 @@ export async function waitForBuildEndAsync(
|
||||
originalSpinnerText = 'Waiting for builds to complete. You can press Ctrl+C to exit.';
|
||||
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
|
||||
}
|
||||
const endTime = new Date().getTime() + timeoutSec * 1000;
|
||||
while (new Date().getTime() <= endTime) {
|
||||
while (true) {
|
||||
const builds = await getBuildsSafelyAsync(buildIds);
|
||||
const { refetch } =
|
||||
builds.length === 1
|
||||
@@ -271,10 +266,6 @@ export async function waitForBuildEndAsync(
|
||||
}
|
||||
await sleepAsync(intervalSec * 1000);
|
||||
}
|
||||
spinner.fail('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
|
||||
async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
|
||||
|
||||
@@ -60,6 +60,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
projectDir,
|
||||
user,
|
||||
env: buildProfile.env,
|
||||
easJsonCliConfig,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Env, Ios } from '@expo/eas-build-job';
|
||||
import { Ios } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
||||
@@ -27,7 +27,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
|
||||
function transformIosSecrets(secrets: {
|
||||
buildCredentials?: Ios.BuildCredentials;
|
||||
env?: Env;
|
||||
}): IosJobSecretsInput {
|
||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||
|
||||
@@ -43,6 +42,5 @@ function transformIosSecrets(secrets: {
|
||||
|
||||
return {
|
||||
buildCredentials,
|
||||
environmentSecrets: secrets.env,
|
||||
};
|
||||
}
|
||||
@@ -94,14 +94,14 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
|
||||
return;
|
||||
}
|
||||
if (deprecationInfo.type === EasBuildDeprecationInfoType.Internal) {
|
||||
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
Log.warn('This command is using API that soon will be deprecated. Upgrade EAS CLI.');
|
||||
Log.warn("Changes won't affect your project config.");
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
|
||||
Log.warn('This command is using API that soon will be deprecated.');
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else {
|
||||
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
|
||||
Log.warn('An unexpected warning was encountered. Report it as a bug:');
|
||||
Log.warn(deprecationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
});
|
||||
|
||||
if (!answer) {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
throw new Error('Commit all changes. Aborting...');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`This operation needs to be run on a clean working tree, please ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
`This operation needs to be run on a clean working tree. ${chalk.bold(
|
||||
'Commit all your changes before proceeding'
|
||||
)}.`
|
||||
);
|
||||
if (nonInteractive) {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
throw new Error('Commit all changes. Aborting...');
|
||||
}
|
||||
const answer = await confirmAsync({
|
||||
message: `Commit changes to git?`,
|
||||
@@ -52,7 +52,7 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
|
||||
if (answer) {
|
||||
await commitPromptAsync({ commitAllFiles: true });
|
||||
} else {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
throw new Error('Commit all changes. Aborting...');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import * as PackageManagerUtils from '@expo/package-manager';
|
||||
import { Command } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
@@ -8,6 +12,7 @@ import {
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
import { learnMore } from '../log';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
@@ -28,8 +33,11 @@ export default abstract class EasCommand extends Command {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
await initAnalyticsAsync();
|
||||
|
||||
if (this.mustBeRunInsideProject) {
|
||||
await this.applyCliConfigAsync();
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await this.applyCliConfigAsync(projectDir);
|
||||
await this.ensureEasCliIsNotInDependenciesAsync(projectDir);
|
||||
}
|
||||
|
||||
if (this.requiresAuthentication) {
|
||||
@@ -53,8 +61,7 @@ export default abstract class EasCommand extends Command {
|
||||
return super.finally(err);
|
||||
}
|
||||
|
||||
private async applyCliConfigAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
private async applyCliConfigAsync(projectDir: string): Promise<void> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const config = await easJsonReader.getCliConfigAsync();
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
@@ -66,4 +73,50 @@ export default abstract class EasCommand extends Command {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
|
||||
let printCliVersionWarning = false;
|
||||
|
||||
const consoleWarn = (msg?: string): void => {
|
||||
if (msg) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(chalk.yellow(msg));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn();
|
||||
}
|
||||
};
|
||||
|
||||
if (await this.isEasCliInDependenciesAsync(projectDir)) {
|
||||
printCliVersionWarning = true;
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
|
||||
}
|
||||
|
||||
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
|
||||
if (maybeRepoRoot !== projectDir && (await this.isEasCliInDependenciesAsync(maybeRepoRoot))) {
|
||||
printCliVersionWarning = true;
|
||||
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
|
||||
}
|
||||
|
||||
if (printCliVersionWarning) {
|
||||
consoleWarn(
|
||||
`It's recommended to use the ${chalk.bold(
|
||||
'"cli.version"'
|
||||
)} field in eas.json to enforce the ${chalk.bold('eas-cli')} version for your project.`
|
||||
);
|
||||
consoleWarn(
|
||||
learnMore('https://github.com/expo/eas-cli#enforcing-eas-cli-version-for-your-project')
|
||||
);
|
||||
consoleWarn();
|
||||
}
|
||||
}
|
||||
|
||||
private async isEasCliInDependenciesAsync(dir: string): Promise<boolean> {
|
||||
const packageJsonPath = path.join(dir, 'package.json');
|
||||
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
return (
|
||||
packageJson?.dependencies?.['eas-cli'] !== undefined ||
|
||||
packageJson?.devDependencies?.['eas-cli'] !== undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export default class BranchCreate extends EasCommand {
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please name the branch:',
|
||||
message: 'Provide a branch name:',
|
||||
initial:
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
|
||||
@@ -115,7 +115,7 @@ export default class BranchDelete extends EasCommand {
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the branch to delete:',
|
||||
message: 'Provide the name of the branch to delete:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class BranchRename extends EasCommand {
|
||||
({ currentName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'currentName',
|
||||
message: 'Please enter the current name of the branch to rename:',
|
||||
message: "Provide the name of the branch you'd like to rename:",
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export default class BranchRename extends EasCommand {
|
||||
({ newName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'newName',
|
||||
message: `Please rename ${currentName}`,
|
||||
message: `Rename ${currentName}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import path from 'path';
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { UserInputResourceClass } from '../../build/types';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { StatuspageServiceName } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
|
||||
interface RawBuildFlags {
|
||||
platform?: string;
|
||||
@@ -95,17 +97,30 @@ export default class Build extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(Build);
|
||||
|
||||
if (rawFlags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
const flags = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
|
||||
await runBuildAndSubmitAsync(projectDir, flags);
|
||||
if (!flags.localBuildOptions.enable) {
|
||||
await maybeWarnAboutEasOutagesAsync(
|
||||
flags.autoSubmit
|
||||
? [StatuspageServiceName.EasBuild, StatuspageServiceName.EasSubmit]
|
||||
: [StatuspageServiceName.EasBuild]
|
||||
);
|
||||
}
|
||||
|
||||
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
|
||||
|
||||
await runBuildAndSubmitAsync(projectDir, flagsWithPlatform);
|
||||
}
|
||||
|
||||
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
|
||||
private sanitizeFlags(
|
||||
flags: RawBuildFlags
|
||||
): Omit<BuildFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform } {
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.local && flags.output) {
|
||||
Errors.error('--output is allowed only for local builds', { exit: 1 });
|
||||
@@ -117,21 +132,11 @@ export default class Build extends EasCommand {
|
||||
Errors.error('--json is allowed only when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
if (flags.local) {
|
||||
if (flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined) {
|
||||
// TODO: implement this
|
||||
Errors.error('Auto-submits are not yet supported when building locally', { exit: 1 });
|
||||
}
|
||||
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
Errors.error('Builds for multiple platforms are not supported with flag --local', {
|
||||
exit: 1,
|
||||
});
|
||||
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
|
||||
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
|
||||
}
|
||||
}
|
||||
const requestedPlatform =
|
||||
flags.platform &&
|
||||
Object.values(RequestedPlatform).includes(flags.platform.toLowerCase() as RequestedPlatform)
|
||||
? (flags.platform.toLowerCase() as RequestedPlatform)
|
||||
: undefined;
|
||||
|
||||
if (flags['skip-credentials-check']) {
|
||||
Log.warnDeprecatedFlag(
|
||||
@@ -176,4 +181,30 @@ export default class Build extends EasCommand {
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
private async ensurePlatformSelectedAsync(
|
||||
flags: Omit<BuildFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform }
|
||||
): Promise<BuildFlags> {
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.requestedPlatform);
|
||||
|
||||
if (flags.localBuildOptions.enable) {
|
||||
if (flags.autoSubmit) {
|
||||
// TODO: implement this
|
||||
Errors.error('Auto-submits are not yet supported when building locally', { exit: 1 });
|
||||
}
|
||||
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
Errors.error('Builds for multiple platforms are not supported with flag --local', {
|
||||
exit: 1,
|
||||
});
|
||||
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
|
||||
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...flags,
|
||||
requestedPlatform,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export default class ChannelCreate extends EasCommand {
|
||||
({ name: channelName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please name the channel:',
|
||||
message: 'Provide a channel name:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default class ChannelDelete extends EasCommand {
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the channel to delete:',
|
||||
message: 'Provide the name of the channel to delete:',
|
||||
validate: (value: any) => (value ? true : 'Channel name may not be empty.'),
|
||||
})
|
||||
).name;
|
||||
|
||||
@@ -127,9 +127,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
|
||||
const existingChannel = await getChannelByNameForAppAsync({ appId: projectId, channelName });
|
||||
if (existingChannel.updateBranches.length > 1) {
|
||||
throw new Error(
|
||||
'There is a rollout in progress. Please manage it with "channel:rollout" instead.'
|
||||
);
|
||||
throw new Error('There is a rollout in progress. Manage it with "channel:rollout" instead.');
|
||||
}
|
||||
|
||||
const branchName = flags.branch ?? (await promptForBranchAsync());
|
||||
@@ -140,7 +138,7 @@ export default class ChannelEdit extends EasCommand {
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
|
||||
`Could not find a branch named "${branchName}". Check what branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
@@ -173,7 +171,7 @@ async function promptForChannelAsync(): Promise<string> {
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the channel to edit:',
|
||||
message: 'Provide the name of the channel to edit:',
|
||||
validate: value => (value ? true : 'The channel name may not be empty.'),
|
||||
});
|
||||
return name;
|
||||
|
||||
@@ -92,7 +92,7 @@ async function startRolloutAsync({
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
|
||||
`Could not find a branch named "${branchName}". Check which branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
@@ -238,7 +238,7 @@ async function endRolloutAsync({
|
||||
});
|
||||
if (!branch) {
|
||||
throw new Error(
|
||||
`Could not find a branch named "${branchName}". Please check what branches exist on this project with ${chalk.bold(
|
||||
`Could not find a branch named "${branchName}". Check which branches exist on this project with ${chalk.bold(
|
||||
'eas branch:list'
|
||||
)}.`
|
||||
);
|
||||
@@ -354,7 +354,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
});
|
||||
if (!channel) {
|
||||
throw new Error(
|
||||
`Could not find a channel named "${channelName}". Please check what channels exist on this project with ${chalk.bold(
|
||||
`Could not find a channel named "${channelName}". Check which channels exist on this project with ${chalk.bold(
|
||||
'eas channel:list'
|
||||
)}.`
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ export default class ChannelView extends EasCommand {
|
||||
({ name: channelName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please name the channel:',
|
||||
message: 'Provide a channel name:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
|
||||
export default class Credentials extends EasCommand {
|
||||
static description = 'manage credentials';
|
||||
|
||||
static flags = {
|
||||
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await new SelectPlatform().runAsync();
|
||||
const { flags } = await this.parse(Credentials);
|
||||
await new SelectPlatform(flags.platform).runAsync();
|
||||
}
|
||||
}
|
||||
@@ -51,11 +51,11 @@ export default class MetadataPull extends EasCommand {
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Your store configuration is ready.
|
||||
Log.log(`🎉 Your store config is ready.
|
||||
|
||||
- Update the ${chalk.bold(relativePath)} file to prepare the app information.
|
||||
- Run ${chalk.bold('eas submit')} or manually upload a new app version to the app stores.
|
||||
- Once the app is uploaded, run ${chalk.bold('eas metadata:push')} to sync the store configuration.
|
||||
- Once the app is uploaded, run ${chalk.bold('eas metadata:push')} to sync the store config.
|
||||
- ${learnMore('https://docs.expo.dev/eas-metadata/introduction/')}`);
|
||||
} catch (error: any) {
|
||||
handleMetadataError(error);
|
||||
|
||||
@@ -3,7 +3,11 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, setProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
fetchProjectIdFromServerAsync,
|
||||
findProjectRootAsync,
|
||||
saveProjectIdToAppConfigAsync,
|
||||
} from '../../project/projectUtils';
|
||||
|
||||
export default class ProjectInit extends EasCommand {
|
||||
static description = 'create or link an EAS project';
|
||||
@@ -20,6 +24,9 @@ export default class ProjectInit extends EasCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
await setProjectIdAsync(projectDir);
|
||||
const projectId = await fetchProjectIdFromServerAsync(exp);
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId);
|
||||
|
||||
Log.withTick(`Linked app.json to project with ID ${chalk.bold(projectId)}`);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export default class EnvironmentSecretList extends EasCommand {
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!projectDir) {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
throw new Error("Run this command inside your project's directory");
|
||||
}
|
||||
|
||||
const secrets = await EnvironmentSecretsQuery.allAsync(projectAccountName, projectId);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { SubmissionFragment } from '../graphql/generated';
|
||||
import { StatuspageServiceName, SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import {
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { getProfilesAsync } from '../utils/profiles';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../utils/statuspageService';
|
||||
|
||||
interface RawCommandFlags {
|
||||
platform?: string;
|
||||
@@ -91,18 +92,23 @@ export default class Submit extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(Submit);
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
|
||||
const flags = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasSubmit]);
|
||||
|
||||
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
|
||||
|
||||
const platforms = toPlatforms(flagsWithPlatform.requestedPlatform);
|
||||
const submissionProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonReader: new EasJsonReader(projectDir),
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
profileName: flagsWithPlatform.profile,
|
||||
});
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
@@ -112,8 +118,8 @@ export default class Submit extends EasCommand {
|
||||
projectDir,
|
||||
projectId,
|
||||
profile: submissionProfile.profile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
archiveFlags: flagsWithPlatform.archiveFlags,
|
||||
nonInteractive: flagsWithPlatform.nonInteractive,
|
||||
});
|
||||
|
||||
if (submissionProfiles.length > 1) {
|
||||
@@ -133,15 +139,17 @@ export default class Submit extends EasCommand {
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
|
||||
if (flags.wait) {
|
||||
if (flagsWithPlatform.wait) {
|
||||
const completedSubmissions = await waitToCompleteAsync(submissions, {
|
||||
verbose: flags.verbose,
|
||||
verbose: flagsWithPlatform.verbose,
|
||||
});
|
||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||
}
|
||||
}
|
||||
|
||||
private async sanitizeFlagsAsync(flags: RawCommandFlags): Promise<CommandFlags> {
|
||||
private sanitizeFlags(
|
||||
flags: RawCommandFlags
|
||||
): Omit<CommandFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform } {
|
||||
const {
|
||||
platform,
|
||||
verbose,
|
||||
@@ -155,15 +163,11 @@ export default class Submit extends EasCommand {
|
||||
Errors.error('--platform is required when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
if (archiveFlags.id || archiveFlags.path || archiveFlags.url) {
|
||||
Errors.error(
|
||||
'--id, --path, and --url params are only supported when performing a single-platform submit',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
const requestedPlatform =
|
||||
flags.platform &&
|
||||
Object.values(RequestedPlatform).includes(flags.platform.toLowerCase() as RequestedPlatform)
|
||||
? (flags.platform.toLowerCase() as RequestedPlatform)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
archiveFlags,
|
||||
@@ -174,4 +178,24 @@ export default class Submit extends EasCommand {
|
||||
nonInteractive,
|
||||
};
|
||||
}
|
||||
|
||||
private async ensurePlatformSelectedAsync(
|
||||
flags: Omit<CommandFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform }
|
||||
): Promise<CommandFlags> {
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.requestedPlatform);
|
||||
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
if (flags.archiveFlags.id || flags.archiveFlags.path || flags.archiveFlags.url) {
|
||||
Errors.error(
|
||||
'--id, --path, and --url params are only supported when performing a single-platform submit',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...flags,
|
||||
requestedPlatform,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PublishUpdateGroupInput,
|
||||
Robot,
|
||||
RootQueryUpdatesByGroupArgs,
|
||||
StatuspageServiceName,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
UpdatePublishMutation,
|
||||
@@ -59,6 +60,7 @@ import {
|
||||
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';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
@@ -228,6 +230,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
'non-interactive': nonInteractive,
|
||||
},
|
||||
} = await this.parse(UpdatePublish);
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
@@ -245,6 +248,8 @@ export default class UpdatePublish extends EasCommand {
|
||||
isPublicConfig: false,
|
||||
});
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasUpdate]);
|
||||
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion);
|
||||
@@ -302,7 +307,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Creating a new one. Please name the new branch:',
|
||||
message: 'No branches found. Provide a branch name:',
|
||||
initial:
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
@@ -404,7 +409,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Please enter an update message.`,
|
||||
message: `Provide an update message.`,
|
||||
initial: `Republish "${oldMessage!}" - group: ${group}`,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
@@ -426,7 +431,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Please enter an update message.`,
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
@@ -757,7 +762,9 @@ async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
|
||||
|
||||
if (configuredURL !== expectedURL) {
|
||||
throw new Error(
|
||||
`The update URL is incorrectly configured for EAS Update. Please set updates.url to ${expectedURL} in your app.json.`
|
||||
`The update URL is incorrectly configured for EAS Update. Set updates.url to ${expectedURL} in your ${chalk.bold(
|
||||
'app.json'
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class BranchView extends EasCommand {
|
||||
({ name: branchInteractive } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the branch whose updates you wish to view:',
|
||||
message: 'Provide the name of the branch whose updates you wish to view:',
|
||||
initial: (await getVcsClient().getBranchNameAsync()) ?? undefined,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
|
||||
@@ -18,6 +18,7 @@ export const jester2: Actor = {
|
||||
|
||||
export const testUsername = jester.username;
|
||||
export const testSlug = 'testApp';
|
||||
export const testProjectId = '7ef93448-3bc7-4b57-be32-99326dcf24f0';
|
||||
export const testBundleIdentifier = 'test.com.app';
|
||||
export const testPackageName = 'test.com.app';
|
||||
export const testExperienceName = `@${testUsername}/${testSlug}`;
|
||||
@@ -35,6 +36,11 @@ export const testAppJson = {
|
||||
sdkVersion: '38.0.0',
|
||||
ios: { bundleIdentifier: testBundleIdentifier },
|
||||
android: { package: testPackageName },
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: testProjectId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const testAppJsonWithDifferentOwner = {
|
||||
|
||||
@@ -54,7 +54,7 @@ export class RemoveKeystore {
|
||||
)
|
||||
);
|
||||
Log.warn(
|
||||
'Please read https://docs.expo.dev/distribution/building-standalone-apps/#if-you-choose-to-build-for-android for more info before proceeding.'
|
||||
'Read https://docs.expo.dev/distribution/building-standalone-apps/#if-you-choose-to-build-for-android for more info before proceeding.'
|
||||
);
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
CommonAndroidAppCredentialsFragment,
|
||||
GoogleServiceAccountKeyFragment,
|
||||
} from '../../../graphql/generated';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { AppQuery } from '../../ios/api/graphql/queries/AppQuery';
|
||||
import { GoogleServiceAccountKey, KeystoreWithType } from '../credentials';
|
||||
import { AndroidAppBuildCredentialsMutation } from './graphql/mutations/AndroidAppBuildCredentialsMutation';
|
||||
import { AndroidAppCredentialsMutation } from './graphql/mutations/AndroidAppCredentialsMutation';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { EasJson } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../log';
|
||||
@@ -19,6 +20,7 @@ export class CredentialsContext {
|
||||
public readonly nonInteractive: boolean;
|
||||
public readonly projectDir: string;
|
||||
public readonly user: Actor;
|
||||
public readonly easJsonCliConfig?: EasJson['cli'];
|
||||
|
||||
private shouldAskAuthenticateAppStore: boolean = true;
|
||||
private resolvedExp?: ExpoConfig;
|
||||
@@ -26,12 +28,14 @@ export class CredentialsContext {
|
||||
constructor(
|
||||
private options: {
|
||||
exp?: ExpoConfig;
|
||||
easJsonCliConfig?: EasJson['cli'];
|
||||
nonInteractive?: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
this.easJsonCliConfig = options.easJsonCliConfig;
|
||||
this.projectDir = options.projectDir;
|
||||
this.user = options.user;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export class MissingCredentialsNonInteractiveError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(
|
||||
message ?? 'Credentials are not set up. Please run this command again in interactive mode.'
|
||||
);
|
||||
super(message ?? 'Credentials are not set up. Run this command again in interactive mode.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, DistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import {
|
||||
CredentialsSource,
|
||||
DistributionType,
|
||||
EasJsonReader,
|
||||
IosEnterpriseProvisioning,
|
||||
} from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { CommonIosAppCredentialsFragment } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findApplicationTarget } from '../../project/ios/target';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { CredentialsContext } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
|
||||
@@ -21,6 +27,12 @@ interface Options {
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
}
|
||||
|
||||
enum PushNotificationSetupOption {
|
||||
YES,
|
||||
NO,
|
||||
NO_DONT_ASK_AGAIN,
|
||||
}
|
||||
|
||||
export default class IosCredentialsProvider {
|
||||
public readonly platform = Platform.IOS;
|
||||
|
||||
@@ -89,13 +101,39 @@ export default class IosCredentialsProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
const confirmSetup = await confirmAsync({
|
||||
message: `Would you like to set up Push Notifications for your project?`,
|
||||
});
|
||||
if (!confirmSetup) {
|
||||
if (ctx.easJsonCliConfig?.promptToConfigurePushNotfications === false) {
|
||||
return null;
|
||||
}
|
||||
return await setupPushKeyAction.runAsync(ctx);
|
||||
|
||||
const setupOption = await selectAsync(
|
||||
`Would you like to set up Push Notifications for your project?`,
|
||||
[
|
||||
{ title: 'Yes', value: PushNotificationSetupOption.YES },
|
||||
{ title: 'No', value: PushNotificationSetupOption.NO },
|
||||
{
|
||||
title: `No, don't ask again (preference will be saved to eas.json)`,
|
||||
value: PushNotificationSetupOption.NO_DONT_ASK_AGAIN,
|
||||
},
|
||||
]
|
||||
);
|
||||
if (setupOption === PushNotificationSetupOption.YES) {
|
||||
return await setupPushKeyAction.runAsync(ctx);
|
||||
} else {
|
||||
if (setupOption === PushNotificationSetupOption.NO_DONT_ASK_AGAIN) {
|
||||
await this.disablePushNotificationsSetupInEasJsonAsync(ctx);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async disablePushNotificationsSetupInEasJsonAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<void> {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
|
||||
const easJson = await fs.readJSON(easJsonPath);
|
||||
easJson.cli = { ...easJson?.cli, promptToConfigurePushNotfications: false };
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
Log.withTick('Updated eas.json');
|
||||
}
|
||||
|
||||
private assertProvisioningProfileType(provisioningProfile: string, targetName?: string): void {
|
||||
|
||||
@@ -147,8 +147,8 @@ const APPLE_DIST_CERTS_TOO_MANY_GENERATED_ERROR = `
|
||||
You can have only ${chalk.underline(
|
||||
'three'
|
||||
)} Apple Distribution Certificates generated on your Apple Developer account.
|
||||
Please revoke the old ones or reuse existing from your other apps.
|
||||
Please remember that Apple Distribution Certificates are not application specific!
|
||||
Revoke the old ones or reuse existing from your other apps.
|
||||
Remember that Apple Distribution Certificates are not application specific!
|
||||
`;
|
||||
|
||||
export async function provideOrGenerateDistributionCertificateAsync(
|
||||
|
||||
@@ -45,7 +45,7 @@ export class SetUpAdhocProvisioningProfile {
|
||||
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, IosDistributionType.AdHoc));
|
||||
} else {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Provisioning profile is not configured correctly. Please run this command again in interactive mode.'
|
||||
'Provisioning profile is not configured correctly. Run this command again in interactive mode.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export class SetUpInternalProvisioningProfile {
|
||||
} else {
|
||||
if (adhocBuildCredentialsExist && enterpriseBuildCredentialsExist) {
|
||||
throw new Error(
|
||||
`You're in non-interactive mode. You have set up both adhoc and universal distribution credentials. Please set the 'enterpriseProvisioning' property (to 'adhoc' or 'universal') in eas.json to choose the credentials to use.`
|
||||
`You're in non-interactive mode. You have set up both adhoc and universal distribution credentials. Set the 'enterpriseProvisioning' property (to 'adhoc' or 'universal') in eas.json to choose the credentials to use.`
|
||||
);
|
||||
} else if (adhocBuildCredentialsExist) {
|
||||
return await this.setupAdhocProvisioningProfileAsync(ctx);
|
||||
@@ -78,7 +78,7 @@ export class SetUpInternalProvisioningProfile {
|
||||
return await this.setupUniversalProvisioningProfileAsync(ctx);
|
||||
} else {
|
||||
throw new Error(
|
||||
`You're in non-interactive mode. EAS CLI couldn't find any credentials suitable for internal distribution. Please run again in interactive mode.`
|
||||
`You're in non-interactive mode. EAS CLI couldn't find any credentials suitable for internal distribution. Run this command again in interactive mode.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export class SetUpProvisioningProfile {
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError(
|
||||
'Provisioning profile is not configured correctly. Please run this command again in interactive mode.'
|
||||
'Provisioning profile is not configured correctly. Run this command again in interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export class SetUpSubmissionCredentials {
|
||||
|
||||
async promptForAppSpecificPasswordAsync(): Promise<string> {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`Please enter your Apple app-specific password.`);
|
||||
Log.log(`Enter your Apple app-specific password.`);
|
||||
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
|
||||
Log.warn(
|
||||
wrapAnsi(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../graphql/generated';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { isWildcardBundleIdentifier } from '../../../project/ios/bundleIdentifier';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { DistributionCertificate, PushKey } from '../appstore/Credentials.types';
|
||||
@@ -33,7 +34,6 @@ import { ApplePushKeyMutation } from './graphql/mutations/ApplePushKeyMutation';
|
||||
import { AppleTeamMutation } from './graphql/mutations/AppleTeamMutation';
|
||||
import { IosAppBuildCredentialsMutation } from './graphql/mutations/IosAppBuildCredentialsMutation';
|
||||
import { IosAppCredentialsMutation } from './graphql/mutations/IosAppCredentialsMutation';
|
||||
import { AppQuery } from './graphql/queries/AppQuery';
|
||||
import { AppStoreConnectApiKeyQuery } from './graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { AppleAppIdentifierQuery } from './graphql/queries/AppleAppIdentifierQuery';
|
||||
import { AppleDeviceQuery } from './graphql/queries/AppleDeviceQuery';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ApiKey, ApiKeyType, UserRole } from '@expo/apple-utils';
|
||||
import { ApiKey, ApiKeyType, UnexpectedResponse, UserRole } from '@expo/apple-utils';
|
||||
|
||||
import {
|
||||
createAscApiKeyAsync,
|
||||
downloadWithRetryAsync,
|
||||
getAscApiKeyAsync,
|
||||
listAscApiKeysAsync,
|
||||
revokeAscApiKeyAsync,
|
||||
} from '../ascApiKey';
|
||||
import { getRequestContext } from '../authenticate';
|
||||
|
||||
jest.mock('@expo/apple-utils');
|
||||
jest.mock('../authenticate');
|
||||
jest.mock('../../../../ora');
|
||||
|
||||
@@ -96,3 +96,38 @@ test(`revokeAscApiKeyAsync`, async () => {
|
||||
expect(mockApiKey.revokeAsync).toBeCalled();
|
||||
expect(result).toEqual(mockAscApiKeyInfo);
|
||||
});
|
||||
|
||||
test(`downloadWithRetryAsync`, async () => {
|
||||
const cacheFailureMessage = `The specified resource does not exist - There is no resource of type 'apiKeys' with id 'TEST-ID'`;
|
||||
|
||||
// complete failure
|
||||
const mockApiKeyWithDownloadError = {
|
||||
...mockApiKey,
|
||||
downloadAsync: jest.fn(() => {
|
||||
throw new UnexpectedResponse(cacheFailureMessage);
|
||||
}),
|
||||
} as unknown as ApiKey;
|
||||
await expect(
|
||||
downloadWithRetryAsync(mockApiKeyWithDownloadError, { minTimeout: 1 }) // stay within jest timeout window
|
||||
).rejects.toThrowError(cacheFailureMessage);
|
||||
// expect to try once and retry 3 times = 4 total
|
||||
expect(mockApiKeyWithDownloadError.downloadAsync as jest.Mock).toBeCalledTimes(7);
|
||||
|
||||
// one time failure
|
||||
const mockApiKeyWithOneTimeDownloadError = mockApiKey as unknown as ApiKey;
|
||||
(mockApiKeyWithOneTimeDownloadError.downloadAsync as jest.Mock).mockClear();
|
||||
(mockApiKeyWithOneTimeDownloadError.downloadAsync as jest.Mock).mockImplementationOnce(() => {
|
||||
throw new UnexpectedResponse(cacheFailureMessage);
|
||||
});
|
||||
const keyP8AfterOneFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
|
||||
minTimeout: 1,
|
||||
});
|
||||
expect(keyP8AfterOneFailure).toBe('super secret');
|
||||
|
||||
// successful case
|
||||
(mockApiKey.downloadAsync as jest.Mock).mockClear();
|
||||
const keyP8NoFailure = await downloadWithRetryAsync(mockApiKeyWithOneTimeDownloadError, {
|
||||
minTimeout: 1,
|
||||
});
|
||||
expect(keyP8NoFailure).toBe('super secret');
|
||||
});
|
||||
+169
-28
@@ -94,6 +94,30 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
'com.apple.security.application-groups': [
|
||||
'group.CY-A5149AC2-49FC-11E7-B3F3-0335A16FFB8D.com.cydia.Extender',
|
||||
],
|
||||
|
||||
'com.apple.developer.fileprovider.testing-mode': true,
|
||||
'com.apple.developer.healthkit.recalibrate-estimates': true,
|
||||
'com.apple.developer.maps': true,
|
||||
'com.apple.developer.user-management': true,
|
||||
'com.apple.developer.networking.custom-protocol': true,
|
||||
'com.apple.developer.system-extension.install': true,
|
||||
'com.apple.developer.push-to-talk': true,
|
||||
'com.apple.developer.driverkit.transport.usb': true,
|
||||
'com.apple.developer.kernel.increased-memory-limit': true,
|
||||
'com.apple.developer.driverkit.communicates-with-drivers': true,
|
||||
'com.apple.developer.media-device-discovery-extension': true,
|
||||
'com.apple.developer.driverkit.allow-third-party-userclients': true,
|
||||
'com.apple.developer.weatherkit': true,
|
||||
'com.apple.developer.on-demand-install-capable': true,
|
||||
'com.apple.developer.driverkit.family.scsicontroller': true,
|
||||
'com.apple.developer.driverkit.family.serial': true,
|
||||
'com.apple.developer.driverkit.family.networking': true,
|
||||
'com.apple.developer.driverkit.family.hid.eventservice': true,
|
||||
'com.apple.developer.driverkit.family.hid.device': true,
|
||||
'com.apple.developer.driverkit': true,
|
||||
'com.apple.developer.driverkit.transport.hid': true,
|
||||
'com.apple.developer.driverkit.family.audio': true,
|
||||
'com.apple.developer.shared-with-you': true,
|
||||
});
|
||||
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
|
||||
@@ -135,36 +159,153 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
},
|
||||
{ capabilityType: 'AUTOFILL_CREDENTIAL_PROVIDER', option: 'ON' },
|
||||
{ capabilityType: 'APP_GROUPS', option: 'ON' },
|
||||
{
|
||||
capabilityType: 'FILEPROVIDER_TESTINGMODE',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'HEALTHKIT_RECALIBRATE_ESTIMATES',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'MAPS',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'USER_MANAGEMENT',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'NETWORK_CUSTOM_PROTOCOL',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'SYSTEM_EXTENSION_INSTALL',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'PUSH_TO_TALK',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_USBTRANSPORT_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'INCREASED_MEMORY_LIMIT',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_COMMUNICATESWITHDRIVERS',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'MEDIA_DEVICE_DISCOVERY',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_ALLOWTHIRDPARTY_USERCLIENTS',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'WEATHERKIT',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'ONDEMANDINSTALL_EXTENSIONS',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_FAMILY_SCSICONTROLLER_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_FAMILY_SERIAL_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_FAMILY_NETWORKING_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_FAMILY_HIDEVENTSERVICE_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_FAMILY_HIDDEVICE_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_PUBLIC',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_TRANSPORT_HID_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'DRIVERKIT_FAMILY_AUDIO_PUB',
|
||||
option: 'ON',
|
||||
},
|
||||
{
|
||||
capabilityType: 'SHARED_WITH_YOU',
|
||||
option: 'ON',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(results.enabled).toStrictEqual([
|
||||
'HealthKit',
|
||||
'Apple Pay Payment Processing',
|
||||
'iCloud',
|
||||
'ClassKit',
|
||||
'Data Protection',
|
||||
'Network Extensions',
|
||||
'Personal VPN',
|
||||
'Hotspot',
|
||||
'Extended Virtual Address Space',
|
||||
'HomeKit',
|
||||
'Multipath',
|
||||
'Wireless Accessory Configuration',
|
||||
'Inter-App Audio',
|
||||
'Wallet',
|
||||
'Fonts',
|
||||
'App Attest',
|
||||
'NFC Tag Reading',
|
||||
'Sign In with Apple',
|
||||
'SiriKit',
|
||||
'Access WiFi Information',
|
||||
'Communication Notifications',
|
||||
'Time Sensitive Notifications',
|
||||
'Group Activities',
|
||||
'Family Controls',
|
||||
'AutoFill Credential Provider',
|
||||
'App Groups',
|
||||
]);
|
||||
expect(results.enabled).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"HealthKit",
|
||||
"Apple Pay Payment Processing",
|
||||
"iCloud",
|
||||
"ClassKit",
|
||||
"Data Protection",
|
||||
"Network Extensions",
|
||||
"Personal VPN",
|
||||
"Hotspot",
|
||||
"Extended Virtual Address Space",
|
||||
"HomeKit",
|
||||
"Multipath",
|
||||
"Wireless Accessory Configuration",
|
||||
"Inter-App Audio",
|
||||
"Wallet",
|
||||
"Fonts",
|
||||
"App Attest",
|
||||
"NFC Tag Reading",
|
||||
"Sign In with Apple",
|
||||
"SiriKit",
|
||||
"Access WiFi Information",
|
||||
"Communication Notifications",
|
||||
"Time Sensitive Notifications",
|
||||
"Group Activities",
|
||||
"Family Controls",
|
||||
"AutoFill Credential Provider",
|
||||
"App Groups",
|
||||
"FileProvider TestingMode",
|
||||
"Recalibrate Estimates",
|
||||
"Maps",
|
||||
"TV Services",
|
||||
"Custom Network Protocol",
|
||||
"System Extension",
|
||||
"Push to Talk",
|
||||
"DriverKit USB Transport (development)",
|
||||
"Increased Memory Limit",
|
||||
"Communicates with Drivers",
|
||||
"Media Device Discovery",
|
||||
"DriverKit Allow Third Party UserClients",
|
||||
"WeatherKit",
|
||||
"On Demand Install Capable for App Clip Extensions",
|
||||
"DriverKit Family SCSIController (development)",
|
||||
"DriverKit Family Serial (development)",
|
||||
"DriverKit Family Networking (development)",
|
||||
"DriverKit Family HID EventService (development)",
|
||||
"DriverKit Family HID Device (development)",
|
||||
"DriverKit for Development",
|
||||
"DriverKit Transport HID (development)",
|
||||
"DriverKit Family Audio (development)",
|
||||
"Shared with You",
|
||||
]
|
||||
`);
|
||||
expect(results.disabled).toStrictEqual([]);
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -153,7 +153,7 @@ describe(syncCapabilityIdentifiersForEntitlementsAsync, () => {
|
||||
(_ctx: any, { identifier }: { identifier: string }): any => {
|
||||
// e2e test with: merchant.expodemo
|
||||
throw new Error(
|
||||
`There is a problem with the request entity - A Merchant ID with Identifier '${identifier}' is not available. Please enter a different string.`
|
||||
`There is a problem with the request entity - A Merchant ID with Identifier '${identifier}' is not available. Enter a different string.`
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -167,6 +167,6 @@ describe(syncCapabilityIdentifiersForEntitlementsAsync, () => {
|
||||
syncCapabilityIdentifiersForEntitlementsAsync(bundleId, {
|
||||
'com.apple.developer.in-app-payments': ['merchant.bacon'],
|
||||
})
|
||||
).rejects.toThrow(/is not available. Please enter a different string/);
|
||||
).rejects.toThrow(/is not available. Enter a different string/);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiKey, ApiKeyProps, ApiKeyType, UserRole } from '@expo/apple-utils';
|
||||
import promiseRetry from 'promise-retry';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { Analytics, SubmissionEvent } from '../../../analytics/events';
|
||||
import Log, { learnMore } from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
|
||||
import { getRequestContext } from './authenticate';
|
||||
@@ -49,6 +51,73 @@ export async function getAscApiKeyAsync(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* There is a bug in Apple's infrastructure that does not propagate newly created objects for a
|
||||
* while. If the key has not propagated and you try to download it, Apple will error saying that
|
||||
* the resource does not exist. We retry with exponential backoff until the key propagates and
|
||||
* is available for download.
|
||||
* */
|
||||
export async function downloadWithRetryAsync(
|
||||
key: ApiKey,
|
||||
{
|
||||
minTimeout = 1000,
|
||||
retries = 6,
|
||||
factor = 2,
|
||||
}: { minTimeout?: number; retries?: number; factor?: number } = {}
|
||||
): Promise<string | null> {
|
||||
const RESOURCE_DOES_NOT_EXIST_MESSAGE =
|
||||
'The specified resource does not exist - There is no resource of type';
|
||||
try {
|
||||
const keyP8 = await promiseRetry(
|
||||
async (retry, number) => {
|
||||
try {
|
||||
return await key.downloadAsync();
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.name === 'UnexpectedAppleResponse' &&
|
||||
e.message.includes(RESOURCE_DOES_NOT_EXIST_MESSAGE)
|
||||
) {
|
||||
const secondsToRetry = Math.pow(factor, number);
|
||||
Log.log(
|
||||
`Received an unexpected response from Apple, retrying in ${secondsToRetry} seconds...`
|
||||
);
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_RETRY, {
|
||||
errorName: e.name,
|
||||
reason: e.message,
|
||||
retry: number,
|
||||
});
|
||||
return retry(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
{
|
||||
retries,
|
||||
factor,
|
||||
minTimeout,
|
||||
}
|
||||
);
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_SUCCESS, {});
|
||||
return keyP8;
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.name === 'UnexpectedAppleResponse' &&
|
||||
e.message.includes(RESOURCE_DOES_NOT_EXIST_MESSAGE)
|
||||
) {
|
||||
Log.warn(
|
||||
`Unable to download Api Key from Apple at this time. Create and upload your key manually by running 'eas credentials' ${learnMore(
|
||||
'https://expo.fyi/creating-asc-api-key'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
Analytics.logEvent(SubmissionEvent.API_KEY_DOWNLOAD_FAIL, {
|
||||
errorName: e.name,
|
||||
reason: e.message,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an App Store Connect API Key.
|
||||
* **Does not support App Store Connect API (CI).**
|
||||
@@ -71,7 +140,7 @@ export async function createAscApiKeyAsync(
|
||||
roles: roles ?? [UserRole.ADMIN],
|
||||
keyType: keyType ?? ApiKeyType.PUBLIC_API,
|
||||
});
|
||||
const keyP8 = await key.downloadAsync();
|
||||
const keyP8 = await downloadWithRetryAsync(key);
|
||||
if (!keyP8) {
|
||||
const { nickname, roles } = key.attributes;
|
||||
const humanReadableKey = `App Store Connect Key '${nickname}' (${
|
||||
|
||||
@@ -484,6 +484,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
{
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_vpn_api
|
||||
name: 'Push Notifications',
|
||||
// com.apple.developer.aps-environment
|
||||
entitlement: 'aps-environment',
|
||||
capability: CapabilityType.PUSH_NOTIFICATIONS,
|
||||
validateOptions: validateDevProdString,
|
||||
@@ -522,6 +523,181 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
validateOptions: validateDevProdString,
|
||||
getOptions: getDefinedOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.coremedia.hls.low-latency',
|
||||
name: 'Low Latency HLS',
|
||||
capability: CapabilityType.HLS_LOW_LATENCY,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.associated-domains.mdm-managed',
|
||||
name: 'MDM Managed Associated Domains',
|
||||
capability: CapabilityType.MDM_MANAGED_ASSOCIATED_DOMAINS,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.fileprovider.testing-mode',
|
||||
name: 'FileProvider TestingMode',
|
||||
capability: CapabilityType.FILE_PROVIDER_TESTING_MODE,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.healthkit.recalibrate-estimates',
|
||||
name: 'Recalibrate Estimates',
|
||||
capability: CapabilityType.HEALTH_KIT_RECALIBRATE_ESTIMATES,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.maps',
|
||||
name: 'Maps',
|
||||
capability: CapabilityType.MAPS,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.user-management',
|
||||
name: 'TV Services',
|
||||
capability: CapabilityType.USER_MANAGEMENT,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.networking.custom-protocol',
|
||||
name: 'Custom Network Protocol',
|
||||
capability: CapabilityType.NETWORK_CUSTOM_PROTOCOL,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.system-extension.install',
|
||||
name: 'System Extension',
|
||||
capability: CapabilityType.SYSTEM_EXTENSION_INSTALL,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.push-to-talk',
|
||||
name: 'Push to Talk',
|
||||
capability: CapabilityType.PUSH_TO_TALK,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.transport.usb',
|
||||
name: 'DriverKit USB Transport (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_USB_TRANSPORT_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.kernel.increased-memory-limit',
|
||||
name: 'Increased Memory Limit',
|
||||
capability: CapabilityType.INCREASED_MEMORY_LIMIT,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.communicates-with-drivers',
|
||||
name: 'Communicates with Drivers',
|
||||
capability: CapabilityType.DRIVER_KIT_COMMUNICATES_WITH_DRIVERS,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.media-device-discovery-extension',
|
||||
name: 'Media Device Discovery',
|
||||
capability: CapabilityType.MEDIA_DEVICE_DISCOVERY,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.allow-third-party-userclients',
|
||||
name: 'DriverKit Allow Third Party UserClients',
|
||||
capability: CapabilityType.DRIVER_KIT_ALLOW_THIRD_PARTY_USER_CLIENTS,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.weatherkit',
|
||||
name: 'WeatherKit',
|
||||
capability: CapabilityType.WEATHER_KIT,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.on-demand-install-capable',
|
||||
name: 'On Demand Install Capable for App Clip Extensions',
|
||||
capability: CapabilityType.ON_DEMAND_INSTALL_EXTENSIONS,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.family.scsicontroller',
|
||||
name: 'DriverKit Family SCSIController (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_FAMILY_SCSI_CONTROLLER_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.family.serial',
|
||||
name: 'DriverKit Family Serial (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_FAMILY_SERIAL_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.family.networking',
|
||||
name: 'DriverKit Family Networking (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_FAMILY_NETWORKING_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.family.hid.eventservice',
|
||||
name: 'DriverKit Family HID EventService (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_FAMILY_HID_EVENT_SERVICE_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.family.hid.device',
|
||||
name: 'DriverKit Family HID Device (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_FAMILY_HID_DEVICE_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit',
|
||||
name: 'DriverKit for Development',
|
||||
capability: CapabilityType.DRIVER_KIT_PUBLIC,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.transport.hid',
|
||||
name: 'DriverKit Transport HID (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_TRANSPORT_HID_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.driverkit.family.audio',
|
||||
name: 'DriverKit Family Audio (development)',
|
||||
capability: CapabilityType.DRIVER_KIT_FAMILY_AUDIO_PUB,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
entitlement: 'com.apple.developer.shared-with-you',
|
||||
name: 'Shared with You',
|
||||
capability: CapabilityType.SHARED_WITH_YOU,
|
||||
validateOptions: validateBooleanOptions,
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
|
||||
// These don't appear to have entitlements, so it's unclear how we can automatically enable / disable them at this time.
|
||||
// TODO: Maybe add a warning about manually enabling features?
|
||||
@@ -534,24 +710,9 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
// },
|
||||
// {
|
||||
// entitlement: '',
|
||||
// name: 'FileProvider TestingMode',
|
||||
// capability: 'FILEPROVIDER_TESTINGMODE',
|
||||
// },
|
||||
// {
|
||||
// entitlement: '',
|
||||
// name: 'HLS Interstitial Previews',
|
||||
// capability: 'HLS_INTERSTITIAL_PREVIEW',
|
||||
// },
|
||||
// {
|
||||
// entitlement: '',
|
||||
// name: 'Low Latency HLS',
|
||||
// capability: 'COREMEDIA_HLS_LOW_LATENCY',
|
||||
// },
|
||||
// {
|
||||
// entitlement: '',
|
||||
// name: 'MDM Managed Associated Domains',
|
||||
// capability: 'MDM_MANAGED_ASSOCIATED_DOMAINS',
|
||||
// },
|
||||
|
||||
// "Game Controllers" doesn't appear in Dev Portal but it does show up in Xcode,
|
||||
// toggling in Xcode causes no network request to be sent.
|
||||
|
||||
@@ -67,7 +67,7 @@ async function getRequiredContractMessagesAsync(
|
||||
// prompt the user to open an issue so we can address the new contract state if it ever appears.
|
||||
// TODO: Maybe a silent analytic would be better
|
||||
Log.error(
|
||||
`\nUnexpected Apple developer contract status "${status}". Please open an issue on https://github.com/expo/eas-cli`
|
||||
`\nUnexpected Apple developer contract status "${status}". Open an issue on https://github.com/expo/eas-cli`
|
||||
);
|
||||
Log.newLine();
|
||||
return { messages: (await getContractMessagesAsync(context)) ?? [], isFatal: false };
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
spinner.fail(
|
||||
`The bundle identifier ${chalk.bold(bundleIdentifier)} is not available to team "${
|
||||
authCtx.team.name
|
||||
}" (${authCtx.team.id}), please change it in your app config and try again.`
|
||||
}" (${authCtx.team.id}), change it in your app config and try again.`
|
||||
);
|
||||
} else {
|
||||
spinner.fail(`Failed to register bundle identifier ${chalk.dim(bundleIdentifier)}`);
|
||||
@@ -196,13 +196,13 @@ export async function ensureAppExistsAsync(
|
||||
} catch (error: any) {
|
||||
if (error.message.match(/An App ID with Identifier '(.*)' is not available/)) {
|
||||
throw new Error(
|
||||
`\nThe bundle identifier "${bundleIdentifier}" is not available to provider "${userAuthCtx.authState?.session.provider.name}. Please change it in your app config and try again.\n`
|
||||
`\nThe bundle identifier "${bundleIdentifier}" is not available to provider "${userAuthCtx.authState?.session.provider.name}. Change it in your app config and try again.\n`
|
||||
);
|
||||
}
|
||||
|
||||
spinner.fail(`Failed to create App Store app ${chalk.dim(name)}`);
|
||||
error.message +=
|
||||
'\nPlease visit https://appstoreconnect.apple.com and resolve any warnings, then try again.';
|
||||
'\nVisit https://appstoreconnect.apple.com and resolve any warnings, then try again.';
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -12,8 +12,8 @@ const { MaxKeysCreatedError } = Keys;
|
||||
|
||||
export const APPLE_KEYS_TOO_MANY_GENERATED_ERROR = `
|
||||
You can have only ${chalk.underline('two')} Apple Keys generated on your Apple Developer account.
|
||||
Please revoke the old ones or reuse existing from your other apps.
|
||||
Please remember that Apple Keys are not application specific!
|
||||
Revoke the old ones or reuse existing from your other apps.
|
||||
Remember that Apple Keys are not application specific!
|
||||
`;
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,10 +4,7 @@ import assert from 'assert';
|
||||
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import {
|
||||
getProjectAccountName,
|
||||
promptToCreateProjectIfNotExistsAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
|
||||
@@ -80,12 +77,6 @@ export class ManageAndroid {
|
||||
while (true) {
|
||||
try {
|
||||
if (ctx.hasProjectContext) {
|
||||
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
|
||||
if (!maybeProjectId) {
|
||||
throw new Error(
|
||||
'Your project must be registered with EAS in order to use the credentials manager.'
|
||||
);
|
||||
}
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
|
||||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
@@ -156,12 +147,10 @@ export class ManageAndroid {
|
||||
buildProfile: BuildProfile<Platform.ANDROID>
|
||||
): Promise<GradleBuildContext | undefined> {
|
||||
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
|
||||
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
|
||||
if (!maybeProjectId) {
|
||||
throw new Error(
|
||||
'Your project must be registered with EAS in order to use the credentials manager.'
|
||||
);
|
||||
}
|
||||
|
||||
// ensure the project exists on the EAS server
|
||||
await getProjectIdAsync(ctx.exp);
|
||||
|
||||
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,7 @@ import {
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import {
|
||||
getProjectAccountName,
|
||||
promptToCreateProjectIfNotExistsAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { Account, findAccountByName } from '../../user/Account';
|
||||
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
|
||||
@@ -175,12 +172,9 @@ export class ManageIos {
|
||||
targets: Target[];
|
||||
}> {
|
||||
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
|
||||
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
|
||||
if (!maybeProjectId) {
|
||||
throw new Error(
|
||||
'Your project must be registered with EAS in order to use the credentials manager.'
|
||||
);
|
||||
}
|
||||
|
||||
// ensure the project exists on the EAS server
|
||||
await getProjectIdAsync(ctx.exp);
|
||||
|
||||
const app = { account, projectName: ctx.exp.slug };
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ export class SelectIosDistributionTypeGraphqlFromBuildProfile {
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'Unable to determine type of internal distribution. Please run this command in interactive mode.'
|
||||
'Unable to determine type of internal distribution. Run this command in interactive mode.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { selectPlatformAsync } from '../../platform';
|
||||
import { ManageAndroid } from './ManageAndroid';
|
||||
import { ManageIos } from './ManageIos';
|
||||
|
||||
export class SelectPlatform {
|
||||
constructor(private readonly flagPlatform?: string) {}
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'platform',
|
||||
message: 'Select platform',
|
||||
choices: [
|
||||
{ value: 'android', title: 'Android' },
|
||||
{ value: 'ios', title: 'iOS' },
|
||||
],
|
||||
});
|
||||
const platform = await selectPlatformAsync(this.flagPlatform);
|
||||
|
||||
if (platform === 'ios') {
|
||||
return await new ManageIos(new SelectPlatform(), process.cwd()).runAsync();
|
||||
return await new ManageIos(new SelectPlatform(platform), process.cwd()).runAsync();
|
||||
}
|
||||
return await new ManageAndroid(new SelectPlatform(), process.cwd()).runAsync();
|
||||
return await new ManageAndroid(new SelectPlatform(platform), process.cwd()).runAsync();
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const EXPERT_PROMPT = (): void => {
|
||||
}
|
||||
Log.warn(`
|
||||
In this mode, we won't be able to make sure that your credentials are valid.
|
||||
Please double check that you're uploading valid files for your app otherwise you may encounter strange errors!
|
||||
Double check that you're uploading valid files for your app otherwise you may encounter strange errors!
|
||||
When building for IOS make sure you've created your App ID on the Apple Developer Portal, that your App ID
|
||||
is in app.json as \`bundleIdentifier\`, and that the provisioning profile you
|
||||
upload matches that Team ID and App ID.
|
||||
|
||||
@@ -18,7 +18,7 @@ Internal distribution means that you won't need upload your app archive to App S
|
||||
Your app archive (.ipa) will be installable on your equipment as long as you sign your application with an adhoc provisiong profile.
|
||||
The provisioning profile needs to contain the UDIDs (unique identifiers) of your iPhones and iPads.
|
||||
|
||||
First of all, please choose the Expo account under which you want to register your devices.
|
||||
First of all, choose the Expo account under which you want to register your devices.
|
||||
Later, authenticate with Apple and choose your desired Apple Team (if your Apple ID has access to multiple teams).`;
|
||||
|
||||
export default class DeviceManager {
|
||||
|
||||
@@ -55,7 +55,7 @@ export const graphqlClient = createUrqlClient({
|
||||
},
|
||||
}) as StricterClient;
|
||||
|
||||
/* Please specify additionalTypenames in your Graphql queries */
|
||||
/* Specify additionalTypenames in your Graphql queries */
|
||||
export interface StricterClient extends Client {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
query<Data = any, Variables extends object = {}>(
|
||||
@@ -70,7 +70,7 @@ export async function withErrorHandlingAsync<T>(promise: Promise<OperationResult
|
||||
|
||||
if (error) {
|
||||
if (error.graphQLErrors.some(e => e?.extensions?.isTransient)) {
|
||||
Log.error(`We've encountered a transient error, please try again shortly.`);
|
||||
Log.error(`We've encountered a transient error. Try again shortly.`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
+171
-22
@@ -280,8 +280,8 @@ export type AccountMutation = {
|
||||
grantActorPermissions: Account;
|
||||
/** Rename this account and the primary user's username if this account is a personal account */
|
||||
rename: Account;
|
||||
/** Requests a refund for the specified charge. Returns true if auto-refund was possible, otherwise requests a manual refund from support and returns false. */
|
||||
requestRefund: Scalars['Boolean'];
|
||||
/** Requests a refund for the specified charge by requesting a manual refund from support */
|
||||
requestRefund?: Maybe<Scalars['Boolean']>;
|
||||
/** Revoke specified Permissions for Actor. Actor must already have at least one permission on the account. */
|
||||
revokeActorPermissions: Account;
|
||||
/**
|
||||
@@ -803,9 +803,23 @@ export type AndroidJobKeystoreInput = {
|
||||
keystorePassword: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AndroidJobOverridesInput = {
|
||||
artifactPath?: InputMaybe<Scalars['String']>;
|
||||
buildType?: InputMaybe<AndroidBuildType>;
|
||||
builderEnvironment?: InputMaybe<AndroidBuilderEnvironmentInput>;
|
||||
cache?: InputMaybe<BuildCacheInput>;
|
||||
developmentClient?: InputMaybe<Scalars['Boolean']>;
|
||||
experimental?: InputMaybe<Scalars['JSONObject']>;
|
||||
gradleCommand?: InputMaybe<Scalars['String']>;
|
||||
releaseChannel?: InputMaybe<Scalars['String']>;
|
||||
secrets?: InputMaybe<AndroidJobSecretsInput>;
|
||||
updates?: InputMaybe<BuildUpdatesInput>;
|
||||
username?: InputMaybe<Scalars['String']>;
|
||||
version?: InputMaybe<AndroidJobVersionInput>;
|
||||
};
|
||||
|
||||
export type AndroidJobSecretsInput = {
|
||||
buildCredentials?: InputMaybe<AndroidJobBuildCredentialsInput>;
|
||||
environmentSecrets?: InputMaybe<Scalars['JSONObject']>;
|
||||
};
|
||||
|
||||
export type AndroidJobVersionInput = {
|
||||
@@ -1932,8 +1946,15 @@ export type BuildMutation = {
|
||||
createIosBuild: CreateBuildResult;
|
||||
/** Delete an EAS Build build */
|
||||
deleteBuild: Build;
|
||||
/** Retry an EAS Build build */
|
||||
/** Retry an Android EAS Build */
|
||||
retryAndroidBuild: Build;
|
||||
/**
|
||||
* Retry an EAS Build build
|
||||
* @deprecated Use retryAndroidBuild and retryIosBuild instead
|
||||
*/
|
||||
retryBuild: Build;
|
||||
/** Retry an iOS EAS Build */
|
||||
retryIosBuild: Build;
|
||||
};
|
||||
|
||||
|
||||
@@ -1963,10 +1984,22 @@ export type BuildMutationDeleteBuildArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type BuildMutationRetryAndroidBuildArgs = {
|
||||
buildId: Scalars['ID'];
|
||||
jobOverrides?: InputMaybe<AndroidJobOverridesInput>;
|
||||
};
|
||||
|
||||
|
||||
export type BuildMutationRetryBuildArgs = {
|
||||
buildId: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type BuildMutationRetryIosBuildArgs = {
|
||||
buildId: Scalars['ID'];
|
||||
jobOverrides?: InputMaybe<IosJobOverridesInput>;
|
||||
};
|
||||
|
||||
export type BuildOrBuildJob = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
@@ -2711,9 +2744,28 @@ export type IosJobInput = {
|
||||
version?: InputMaybe<IosJobVersionInput>;
|
||||
};
|
||||
|
||||
export type IosJobOverridesInput = {
|
||||
artifactPath?: InputMaybe<Scalars['String']>;
|
||||
buildConfiguration?: InputMaybe<Scalars['String']>;
|
||||
/** @deprecated */
|
||||
buildType?: InputMaybe<IosBuildType>;
|
||||
builderEnvironment?: InputMaybe<IosBuilderEnvironmentInput>;
|
||||
cache?: InputMaybe<BuildCacheInput>;
|
||||
developmentClient?: InputMaybe<Scalars['Boolean']>;
|
||||
/** @deprecated */
|
||||
distribution?: InputMaybe<DistributionType>;
|
||||
experimental?: InputMaybe<Scalars['JSONObject']>;
|
||||
releaseChannel?: InputMaybe<Scalars['String']>;
|
||||
scheme?: InputMaybe<Scalars['String']>;
|
||||
secrets?: InputMaybe<IosJobSecretsInput>;
|
||||
simulator?: InputMaybe<Scalars['Boolean']>;
|
||||
updates?: InputMaybe<BuildUpdatesInput>;
|
||||
username?: InputMaybe<Scalars['String']>;
|
||||
version?: InputMaybe<IosJobVersionInput>;
|
||||
};
|
||||
|
||||
export type IosJobSecretsInput = {
|
||||
buildCredentials?: InputMaybe<Array<InputMaybe<IosJobTargetCredentialsInput>>>;
|
||||
environmentSecrets?: InputMaybe<Scalars['JSONObject']>;
|
||||
};
|
||||
|
||||
export type IosJobTargetCredentialsInput = {
|
||||
@@ -3215,8 +3267,6 @@ export type RootMutationBuildJobArgs = {
|
||||
|
||||
export type RootQuery = {
|
||||
__typename?: 'RootQuery';
|
||||
/** Top-level query object for querying GitHub App information and resources it has access to. */
|
||||
GitHubApp: GitHubAppQuery;
|
||||
/**
|
||||
* This is a placeholder field
|
||||
* @deprecated Not used.
|
||||
@@ -3251,6 +3301,8 @@ export type RootQuery = {
|
||||
clientBuilds: ClientBuildQuery;
|
||||
/** Top-level query object for querying Experimentation configuration. */
|
||||
experimentation: ExperimentationQuery;
|
||||
/** Top-level query object for querying GitHub App information and resources it has access to. */
|
||||
githubApp: GitHubAppQuery;
|
||||
/** Top-level query object for querying Stripe Invoices. */
|
||||
invoice: InvoiceQuery;
|
||||
/**
|
||||
@@ -3265,6 +3317,8 @@ export type RootQuery = {
|
||||
meActor?: Maybe<Actor>;
|
||||
project: ProjectQuery;
|
||||
snack: SnackQuery;
|
||||
/** Top-level query object for querying Expo status page services. */
|
||||
statuspageService: StatuspageServiceQuery;
|
||||
submissions: SubmissionQuery;
|
||||
/** fetch all updates in a group */
|
||||
updatesByGroup: Array<Update>;
|
||||
@@ -3424,6 +3478,100 @@ export enum StandardOffer {
|
||||
YearlySub = 'YEARLY_SUB'
|
||||
}
|
||||
|
||||
/** Incident for a given component from Expo status page API. */
|
||||
export type StatuspageIncident = {
|
||||
__typename?: 'StatuspageIncident';
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['ID'];
|
||||
/** Impact of an incident from Expo status page. */
|
||||
impact: StatuspageIncidentImpact;
|
||||
name: Scalars['String'];
|
||||
resolvedAt?: Maybe<Scalars['DateTime']>;
|
||||
/** Shortlink to the incident from Expo status page. */
|
||||
shortlink: Scalars['String'];
|
||||
/** Current status of an incident from Expo status page. */
|
||||
status: StatuspageIncidentStatus;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
/** List of all updates for an incident from Expo status page. */
|
||||
updates: Array<StatuspageIncidentUpdate>;
|
||||
};
|
||||
|
||||
/** Possible Incident impact values from Expo status page API. */
|
||||
export enum StatuspageIncidentImpact {
|
||||
Critical = 'CRITICAL',
|
||||
Maintenance = 'MAINTENANCE',
|
||||
Major = 'MAJOR',
|
||||
Minor = 'MINOR',
|
||||
None = 'NONE'
|
||||
}
|
||||
|
||||
/** Possible Incident statuses from Expo status page API. */
|
||||
export enum StatuspageIncidentStatus {
|
||||
Completed = 'COMPLETED',
|
||||
Identified = 'IDENTIFIED',
|
||||
Investigating = 'INVESTIGATING',
|
||||
InProgress = 'IN_PROGRESS',
|
||||
Monitoring = 'MONITORING',
|
||||
Resolved = 'RESOLVED',
|
||||
Scheduled = 'SCHEDULED',
|
||||
Verifying = 'VERIFYING'
|
||||
}
|
||||
|
||||
/** Update for an Incident from Expo status page API. */
|
||||
export type StatuspageIncidentUpdate = {
|
||||
__typename?: 'StatuspageIncidentUpdate';
|
||||
/** Text of an update from Expo status page. */
|
||||
body: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['ID'];
|
||||
/** Status set at the moment of update. */
|
||||
status: StatuspageIncidentStatus;
|
||||
};
|
||||
|
||||
/** Service monitored by Expo status page. */
|
||||
export type StatuspageService = {
|
||||
__typename?: 'StatuspageService';
|
||||
/** Description of a service from Expo status page. */
|
||||
description?: Maybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
/**
|
||||
* List of last inicdents for a service from Expo status page (we always query for 50 latest incidents for all services)
|
||||
* sorted by createdAt field in descending order.
|
||||
*/
|
||||
incidents: Array<StatuspageIncident>;
|
||||
/** Name of a service monitored by Expo status page. */
|
||||
name: StatuspageServiceName;
|
||||
/** Current status of a service from Expo status page. */
|
||||
status: StatuspageServiceStatus;
|
||||
};
|
||||
|
||||
/** Name of a service monitored by Expo status page. */
|
||||
export enum StatuspageServiceName {
|
||||
EasBuild = 'EAS_BUILD',
|
||||
EasSubmit = 'EAS_SUBMIT',
|
||||
EasUpdate = 'EAS_UPDATE'
|
||||
}
|
||||
|
||||
export type StatuspageServiceQuery = {
|
||||
__typename?: 'StatuspageServiceQuery';
|
||||
/** Query services from Expo status page by names. */
|
||||
byServiceNames: Array<StatuspageService>;
|
||||
};
|
||||
|
||||
|
||||
export type StatuspageServiceQueryByServiceNamesArgs = {
|
||||
serviceNames: Array<StatuspageServiceName>;
|
||||
};
|
||||
|
||||
/** Possible statuses for a service. */
|
||||
export enum StatuspageServiceStatus {
|
||||
DegradedPerformance = 'DEGRADED_PERFORMANCE',
|
||||
MajorOutage = 'MAJOR_OUTAGE',
|
||||
Operational = 'OPERATIONAL',
|
||||
PartialOutage = 'PARTIAL_OUTAGE',
|
||||
UnderMaintenance = 'UNDER_MAINTENANCE'
|
||||
}
|
||||
|
||||
export type StripeCoupon = {
|
||||
__typename?: 'StripeCoupon';
|
||||
amountOff?: Maybe<Scalars['String']>;
|
||||
@@ -4471,13 +4619,6 @@ export type SetAppStoreConnectApiKeyForSubmissionsMutationVariables = Exact<{
|
||||
|
||||
export type SetAppStoreConnectApiKeyForSubmissionsMutation = { __typename?: 'RootMutation', iosAppCredentials: { __typename?: 'IosAppCredentialsMutation', setAppStoreConnectApiKeyForSubmissions: { __typename?: 'IosAppCredentials', id: string, iosAppBuildCredentialsList: Array<{ __typename?: 'IosAppBuildCredentials', id: string, iosDistributionType: IosDistributionType, distributionCertificate?: { __typename?: 'AppleDistributionCertificate', id: string, certificateP12?: string | null, certificatePassword?: string | null, serialNumber: string, developerPortalIdentifier?: string | null, validityNotBefore: any, validityNotAfter: any, updatedAt: any, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, iosAppBuildCredentialsList: Array<{ __typename?: 'IosAppBuildCredentials', id: string, iosAppCredentials: { __typename?: 'IosAppCredentials', id: string, app: { __typename?: 'App', id: string, fullName: string, slug: string }, appleAppIdentifier: { __typename?: 'AppleAppIdentifier', id: string, bundleIdentifier: string } }, provisioningProfile?: { __typename?: 'AppleProvisioningProfile', id: string, developerPortalIdentifier?: string | null } | null }> } | null, provisioningProfile?: { __typename?: 'AppleProvisioningProfile', id: string, expiration: any, developerPortalIdentifier?: string | null, provisioningProfile?: string | null, updatedAt: any, status: string, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, appleDevices: Array<{ __typename?: 'AppleDevice', id: string, identifier: string, name?: string | null, model?: string | null, deviceClass?: AppleDeviceClass | null }> } | null }>, app: { __typename?: 'App', id: string, fullName: string, slug: string }, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, appleAppIdentifier: { __typename?: 'AppleAppIdentifier', id: string, bundleIdentifier: string }, pushKey?: { __typename?: 'ApplePushKey', id: string, keyIdentifier: string, updatedAt: any, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null, iosAppCredentialsList: Array<{ __typename?: 'IosAppCredentials', id: string, app: { __typename?: 'App', id: string, fullName: string, slug: string }, appleAppIdentifier: { __typename?: 'AppleAppIdentifier', id: string, bundleIdentifier: string } }> } | null, appStoreConnectApiKeyForSubmissions?: { __typename?: 'AppStoreConnectApiKey', id: string, issuerIdentifier: string, keyIdentifier: string, name?: string | null, roles?: Array<AppStoreConnectUserRole> | null, createdAt: any, updatedAt: any, appleTeam?: { __typename?: 'AppleTeam', id: string, appleTeamIdentifier: string, appleTeamName?: string | null } | null } | null } } };
|
||||
|
||||
export type AppByFullNameQueryVariables = Exact<{
|
||||
fullName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AppByFullNameQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byFullName: { __typename?: 'App', id: string, fullName: string, slug: string } } };
|
||||
|
||||
export type AppStoreConnectApiKeyByAccountQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
}>;
|
||||
@@ -4722,6 +4863,13 @@ export type DeleteWebhookMutationVariables = Exact<{
|
||||
|
||||
export type DeleteWebhookMutation = { __typename?: 'RootMutation', webhook: { __typename?: 'WebhookMutation', deleteWebhook: { __typename?: 'DeleteWebhookResult', id: string } } };
|
||||
|
||||
export type AppByFullNameQueryVariables = Exact<{
|
||||
fullName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AppByFullNameQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byFullName: { __typename?: 'App', id: string, fullName: string, slug: string } } };
|
||||
|
||||
export type LatestAppVersionQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
platform: AppPlatform;
|
||||
@@ -4794,14 +4942,6 @@ export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
|
||||
export type EnvironmentSecretsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, createdAt: any }> } } };
|
||||
|
||||
export type ProjectByUsernameAndSlugQueryVariables = Exact<{
|
||||
username: Scalars['String'];
|
||||
slug: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ProjectByUsernameAndSlugQuery = { __typename?: 'RootQuery', project: { __typename?: 'ProjectQuery', byUsernameAndSlug: { __typename?: 'App', id: string } | { __typename?: 'Snack', id: string } } };
|
||||
|
||||
export type GetAssetMetadataQueryVariables = Exact<{
|
||||
storageKeys: Array<Scalars['String']> | Scalars['String'];
|
||||
}>;
|
||||
@@ -4816,6 +4956,13 @@ export type GetAssetLimitPerUpdateGroupForAppQueryVariables = Exact<{
|
||||
|
||||
export type GetAssetLimitPerUpdateGroupForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, assetLimitPerUpdateGroup: number } } };
|
||||
|
||||
export type StatuspageServiceByServiceNamesQueryVariables = Exact<{
|
||||
serviceNames: Array<StatuspageServiceName> | StatuspageServiceName;
|
||||
}>;
|
||||
|
||||
|
||||
export type StatuspageServiceByServiceNamesQuery = { __typename?: 'RootQuery', statuspageService: { __typename?: 'StatuspageServiceQuery', byServiceNames: Array<{ __typename?: 'StatuspageService', id: string, name: StatuspageServiceName, status: StatuspageServiceStatus, incidents: Array<{ __typename?: 'StatuspageIncident', id: string, status: StatuspageIncidentStatus, name: string, impact: StatuspageIncidentImpact, shortlink: string }> }> } };
|
||||
|
||||
export type SubmissionsByIdQueryVariables = Exact<{
|
||||
submissionId: Scalars['ID'];
|
||||
}>;
|
||||
@@ -4881,6 +5028,8 @@ export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, s
|
||||
|
||||
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, createdAt: any };
|
||||
|
||||
export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id: string, name: StatuspageServiceName, status: StatuspageServiceStatus, incidents: Array<{ __typename?: 'StatuspageIncident', id: string, status: StatuspageIncidentStatus, name: string, impact: StatuspageIncidentImpact, shortlink: string }> };
|
||||
|
||||
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 UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, createdAt: any, message?: string | null, runtimeVersion: string, group: string, platform: string, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'User', username: string, id: string } | null }> };
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ import assert from 'assert';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
|
||||
import { AppByFullNameQuery, AppFragment } from '../../../../../graphql/generated';
|
||||
import { AppFragmentNode } from '../../../../../graphql/types/App';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { AppByFullNameQuery, AppFragment } from '../generated';
|
||||
import { AppFragmentNode } from '../types/App';
|
||||
|
||||
export const AppQuery = {
|
||||
async byFullNameAsync(fullName: string): Promise<AppFragment> {
|
||||
@@ -75,11 +75,11 @@ export const EnvironmentSecretsQuery = {
|
||||
},
|
||||
async allAsync(
|
||||
projectAccountName: string,
|
||||
projectFullName: string
|
||||
projectId: string
|
||||
): Promise<EnvironmentSecretWithScope[]> {
|
||||
const [accountSecrets, appSecrets] = await Promise.all([
|
||||
this.byAccountNameAsync(projectAccountName),
|
||||
this.byAppIdAsync(projectFullName),
|
||||
this.byAppIdAsync(projectId),
|
||||
]);
|
||||
|
||||
return [
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { Project, ProjectByUsernameAndSlugQuery } from '../generated';
|
||||
|
||||
type ProjectQueryResult = Pick<Project, 'id'>;
|
||||
|
||||
export const ProjectQuery = {
|
||||
async byUsernameAndSlugAsync(username: string, slug: string): Promise<ProjectQueryResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<ProjectByUsernameAndSlugQuery>(
|
||||
gql`
|
||||
query ProjectByUsernameAndSlugQuery($username: String!, $slug: String!) {
|
||||
project {
|
||||
byUsernameAndSlug(username: $username, slug: $slug) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ username, slug },
|
||||
{
|
||||
additionalTypenames: ['App', 'Snack'] /* typenames that use the Project type*/,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.project.byUsernameAndSlug;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
StatuspageServiceByServiceNamesQuery,
|
||||
StatuspageServiceByServiceNamesQueryVariables,
|
||||
StatuspageServiceFragment,
|
||||
StatuspageServiceName,
|
||||
} from '../generated';
|
||||
import { StatuspageServiceFragmentNode } from '../types/StatuspageService';
|
||||
|
||||
export const StatuspageServiceQuery = {
|
||||
async statuspageServicesAsync(
|
||||
serviceNames: StatuspageServiceName[]
|
||||
): Promise<StatuspageServiceFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<StatuspageServiceByServiceNamesQuery, StatuspageServiceByServiceNamesQueryVariables>(
|
||||
gql`
|
||||
query StatuspageServiceByServiceNamesQuery($serviceNames: [StatuspageServiceName!]!) {
|
||||
statuspageService {
|
||||
byServiceNames(serviceNames: $serviceNames) {
|
||||
id
|
||||
...StatuspageServiceFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(StatuspageServiceFragmentNode)}
|
||||
`,
|
||||
{ serviceNames },
|
||||
{
|
||||
additionalTypenames: ['StatuspageService', 'StatuspageIncident'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.statuspageService.byServiceNames;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const StatuspageServiceFragmentNode = gql`
|
||||
fragment StatuspageServiceFragment on StatuspageService {
|
||||
id
|
||||
name
|
||||
status
|
||||
incidents {
|
||||
id
|
||||
status
|
||||
name
|
||||
impact
|
||||
shortlink
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,128 @@
|
||||
import path from 'path';
|
||||
|
||||
import { getStaticConfigFilePath, loadConfigAsync } from '../config';
|
||||
import { MetadataValidationError } from '../errors';
|
||||
|
||||
describe(getStaticConfigFilePath, () => {
|
||||
const projectDir = '/app';
|
||||
|
||||
it(`returns same file for store.config.json`, () => {
|
||||
const metadataPath = 'store.config.json';
|
||||
const expectedFile = path.join(projectDir, metadataPath);
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
});
|
||||
|
||||
it(`returns store.config.json for store.config.js`, () => {
|
||||
const metadataPath = 'store.config.js';
|
||||
const expectedFile = path.join(projectDir, 'store.config.json');
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
});
|
||||
|
||||
it(`returns store.staging.json file for store.staging.js`, () => {
|
||||
const metadataPath = 'store.staging.js';
|
||||
const expectedFile = path.join(projectDir, 'store.staging.json');
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
});
|
||||
|
||||
// This shouldn't be used IRL, but it tests if this function is working properly
|
||||
it(`returns custom-name.json file for custom-name.js`, () => {
|
||||
const metadataPath = 'custom-name.js';
|
||||
const expectedFile = path.join(projectDir, 'custom-name.json');
|
||||
|
||||
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe(loadConfigAsync, () => {
|
||||
const projectDir = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
it(`throws when file doesn't exist`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'doesnt-exist.json' })
|
||||
).rejects.toThrow('file not found');
|
||||
});
|
||||
|
||||
it(`throws when validation errors are found without skipping validation`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json' })
|
||||
).rejects.toThrow('errors found');
|
||||
});
|
||||
|
||||
it(`returns config from "store.config.json"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'store.config.json' })
|
||||
).resolves.toHaveProperty('apple.copyright', 'ACME');
|
||||
});
|
||||
|
||||
it(`returns config from "store.config.js"`, async () => {
|
||||
const year = new Date().getFullYear();
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'store.config.js' })
|
||||
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
|
||||
});
|
||||
|
||||
it(`returns config from "store.function.js"`, async () => {
|
||||
const year = new Date().getFullYear();
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'store.function.js' })
|
||||
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
|
||||
});
|
||||
|
||||
it(`returns config from "store.async.js"`, async () => {
|
||||
const year = new Date().getFullYear();
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'store.async.js' })
|
||||
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.config.json`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json', skipValidation: true })
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.config.js`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.js', skipValidation: true })
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.function.js`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.function.js', skipValidation: true })
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`returns invalid config from "invalid.async.js`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid.async.js', skipValidation: true })
|
||||
).resolves.toMatchObject({ configVersion: -1 });
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.config.json"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.json' })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.config.js"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.js' })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.function.js"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.function.js' })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
|
||||
it(`throws invalid config type from "invalid-type.async.js"`, async () => {
|
||||
await expect(
|
||||
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.async.js' })
|
||||
).rejects.toThrow(MetadataValidationError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = async function () {
|
||||
return await Promise.resolve(null);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = 1337;
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"configVersion": -1
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = function () {
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = async function () {
|
||||
return await Promise.resolve({ configVersion: -1 });
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
configVersion: -1,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"configVersion": -1
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = function () {
|
||||
return { configVersion: -1 };
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = async function () {
|
||||
const config = require('./store.config.json');
|
||||
config.apple.copyright = `${new Date().getFullYear()} ACME`;
|
||||
return await Promise.resolve(config);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const config = require('./store.config.json');
|
||||
|
||||
config.apple.copyright = `${new Date().getFullYear()} ACME`;
|
||||
|
||||
module.exports = config;
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"configVersion": 0,
|
||||
"apple": {
|
||||
"copyright": "ACME",
|
||||
"info": {
|
||||
"en-US": {
|
||||
"title": "App title",
|
||||
"subtitle": "Subtitle for your app",
|
||||
"description": "A longer description of what your app does",
|
||||
"keywords": [
|
||||
"keyword"
|
||||
],
|
||||
"releaseNotes": "Bug fixes and improved stability",
|
||||
"promoText": "Short tagline for your app",
|
||||
"marketingUrl": "https://example.com/en/marketing",
|
||||
"supportUrl": "https://example.com/en/support",
|
||||
"privacyPolicyUrl": "https://example.com/en/privacy",
|
||||
"privacyChoicesUrl": "https://example.com/en/choices"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = function () {
|
||||
const config = require('./store.config.json');
|
||||
config.apple.copyright = `${new Date().getFullYear()} ACME`;
|
||||
return config;
|
||||
};
|
||||
@@ -1,9 +1,12 @@
|
||||
import Ajv from 'ajv';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { AppleConfigReader } from './apple/config/reader';
|
||||
import { AppleConfigWriter } from './apple/config/writer';
|
||||
import { AppleMetadata } from './apple/types';
|
||||
import { MetadataValidationError } from './errors';
|
||||
|
||||
export interface MetadataConfig {
|
||||
/** The store configuration version */
|
||||
@@ -12,6 +15,68 @@ export interface MetadataConfig {
|
||||
apple?: AppleMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the dynamic config from the user.
|
||||
* It supports methods, async methods, or objects (json).
|
||||
*/
|
||||
async function resolveDynamicConfigAsync(configFile: string): Promise<unknown> {
|
||||
const userConfigOrFunction = await import(configFile).then(file => file.default ?? file);
|
||||
|
||||
return typeof userConfigOrFunction === 'function'
|
||||
? await userConfigOrFunction()
|
||||
: userConfigOrFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the static configuration file path, based on the metadata context.
|
||||
* This uses any custom name provided, but swaps out the extension for `.json`.
|
||||
*/
|
||||
export function getStaticConfigFilePath({
|
||||
projectDir,
|
||||
metadataPath,
|
||||
}: {
|
||||
projectDir: string;
|
||||
metadataPath: string;
|
||||
}): string {
|
||||
const configFile = path.join(projectDir, metadataPath);
|
||||
const configExtension = path.extname(configFile);
|
||||
|
||||
return path.join(projectDir, `${path.basename(configFile, configExtension)}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the store configuration from a metadata context.
|
||||
* This can load `.json` and `.js` config files, using `require`.
|
||||
* It throws MetadataValidationErrors when the file doesn't exist, or contains errors.
|
||||
* The user is prompted to try anyway when errors are found.
|
||||
*/
|
||||
export async function loadConfigAsync({
|
||||
projectDir,
|
||||
metadataPath,
|
||||
skipValidation = false,
|
||||
}: {
|
||||
projectDir: string;
|
||||
metadataPath: string;
|
||||
skipValidation?: boolean;
|
||||
}): Promise<MetadataConfig> {
|
||||
const configFile = path.join(projectDir, metadataPath);
|
||||
if (!(await fs.pathExists(configFile))) {
|
||||
throw new MetadataValidationError(`Metadata store config file not found: "${configFile}"`);
|
||||
}
|
||||
|
||||
const configData = await resolveDynamicConfigAsync(configFile);
|
||||
|
||||
if (!skipValidation) {
|
||||
const { valid, errors: validationErrors } = validateConfig(configData);
|
||||
|
||||
if (!valid) {
|
||||
throw new MetadataValidationError(`Metadata store config errors found`, validationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
return configData as MetadataConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the JSON Schema validation to normalize defaults and flag early config errors.
|
||||
* This includes validating the known store limitations for every configurable property.
|
||||
|
||||
@@ -6,7 +6,7 @@ import Log from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { AppleData } from './apple/data';
|
||||
import { createAppleTasks } from './apple/tasks';
|
||||
import { createAppleWriter } from './config';
|
||||
import { createAppleWriter, getStaticConfigFilePath } from './config';
|
||||
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
|
||||
import { MetadataDownloadError, MetadataValidationError } from './errors';
|
||||
import { subscribeTelemetry } from './utils/telemetry';
|
||||
@@ -16,15 +16,16 @@ import { subscribeTelemetry } from './utils/telemetry';
|
||||
* Note, only App Store is supported at this time.
|
||||
*/
|
||||
export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promise<string> {
|
||||
const filePath = path.resolve(metadataCtx.projectDir, metadataCtx.metadataPath);
|
||||
const filePath = getStaticConfigFilePath(metadataCtx);
|
||||
const fileExists = await fs.pathExists(filePath);
|
||||
|
||||
if (fileExists) {
|
||||
const filePathRelative = path.relative(metadataCtx.projectDir, filePath);
|
||||
const overwrite = await confirmAsync({
|
||||
message: `Do you want to overwrite the existing store configuration "${metadataCtx.metadataPath}"?`,
|
||||
message: `Do you want to overwrite the existing "${filePathRelative}"?`,
|
||||
});
|
||||
if (!overwrite) {
|
||||
throw new MetadataValidationError(`Store configuration already exists at "${filePath}"`);
|
||||
throw new MetadataValidationError(`Store config already exists at "${filePath}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +36,7 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Downloading App Store configuration...');
|
||||
Log.log('Downloading App Store config...');
|
||||
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleWriter();
|
||||
@@ -59,7 +60,7 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeJson(filePath, config.toSchema(), { spaces: 2 });
|
||||
await fs.writeJSON(filePath, config.toSchema(), { spaces: 2 });
|
||||
} finally {
|
||||
unsubscribeTelemetry();
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ export function handleMetadataError(error: Error): void {
|
||||
Log.error(error.errors.map(err => err.message).join('\n\n'));
|
||||
}
|
||||
Log.newLine();
|
||||
Log.log('Please check the logs for any configuration issues.');
|
||||
Log.log('If this issue persists, please open a new issue at:');
|
||||
Log.log('Check the logs for any configuration issues.');
|
||||
Log.log('If this issue persists, open a new issue at:');
|
||||
// TODO: add execution ID to the issue template link
|
||||
Log.log(link('https://github.com/expo/eas-cli'));
|
||||
return;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { MetadataEvent } from '../analytics/events';
|
||||
import Log from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { AppleData } from './apple/data';
|
||||
import { createAppleTasks } from './apple/tasks';
|
||||
import { createAppleReader, validateConfig } from './config';
|
||||
import { MetadataConfig, createAppleReader, loadConfigAsync } from './config';
|
||||
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
|
||||
import { MetadataUploadError, MetadataValidationError, logMetadataValidationError } from './errors';
|
||||
import { subscribeTelemetry } from './utils/telemetry';
|
||||
@@ -18,28 +15,7 @@ import { subscribeTelemetry } from './utils/telemetry';
|
||||
export async function uploadMetadataAsync(
|
||||
metadataCtx: MetadataContext
|
||||
): Promise<{ appleLink: string }> {
|
||||
const filePath = path.resolve(metadataCtx.projectDir, metadataCtx.metadataPath);
|
||||
if (!(await fs.pathExists(filePath))) {
|
||||
throw new MetadataValidationError(`Store configuration file not found "${filePath}"`);
|
||||
}
|
||||
|
||||
const fileData = await fs.readJson(filePath);
|
||||
const { valid, errors: validationErrors } = validateConfig(fileData);
|
||||
if (!valid) {
|
||||
const error = new MetadataValidationError(`Store configuration errors found`, validationErrors);
|
||||
logMetadataValidationError(error);
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
'Without further updates, the current store configuration may fail to be synchronized with the App Store or pass App Store review.'
|
||||
);
|
||||
const attempt = await confirmAsync({
|
||||
message: 'Do you still want to push the store configuration?',
|
||||
});
|
||||
if (!attempt) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const storeConfig = await loadConfigWithValidationPromptAsync(metadataCtx);
|
||||
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
|
||||
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
|
||||
MetadataEvent.APPLE_METADATA_UPLOAD,
|
||||
@@ -50,7 +26,7 @@ export async function uploadMetadataAsync(
|
||||
Log.log('Uploading App Store configuration...');
|
||||
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleReader(fileData);
|
||||
const config = createAppleReader(storeConfig);
|
||||
const tasks = createAppleTasks(metadataCtx, {
|
||||
// We need to resolve a different version as soon as possible.
|
||||
// This version is the parent model of all changes we are going to push.
|
||||
@@ -83,3 +59,25 @@ export async function uploadMetadataAsync(
|
||||
|
||||
return { appleLink: `https://appstoreconnect.apple.com/apps/${app.id}/appstore` };
|
||||
}
|
||||
|
||||
async function loadConfigWithValidationPromptAsync(
|
||||
metadataCtx: MetadataContext
|
||||
): Promise<MetadataConfig> {
|
||||
try {
|
||||
return await loadConfigAsync(metadataCtx);
|
||||
} catch (error) {
|
||||
if (error instanceof MetadataValidationError) {
|
||||
logMetadataValidationError(error);
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
'Without further updates, the current store configuration can fail to be synchronized with the App Store or pass App Store review.'
|
||||
);
|
||||
|
||||
if (await confirmAsync({ message: 'Do you still want to push the store configuration?' })) {
|
||||
return await loadConfigAsync({ ...metadataCtx, skipValidation: true });
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
import { getConfig, modifyConfigAsync } from '@expo/config';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { Actor, getUserAsync } from '../../user/User';
|
||||
import {
|
||||
ensureProjectExistsAsync,
|
||||
findProjectIdByAccountNameAndSlugNullableAsync,
|
||||
} from '../ensureProjectExists';
|
||||
import { ensureProjectExistsAsync } from '../ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountName,
|
||||
getProjectAccountNameAsync,
|
||||
promptToCreateProjectIfNotExistsAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../projectUtils';
|
||||
|
||||
jest.mock('@expo/config');
|
||||
@@ -20,6 +17,10 @@ jest.mock('../../prompts');
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../ensureProjectExists');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe(findProjectRootAsync, () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
@@ -33,7 +34,7 @@ describe(findProjectRootAsync, () => {
|
||||
'/app'
|
||||
);
|
||||
await expect(findProjectRootAsync({ cwd: '/app' })).rejects.toThrow(
|
||||
'Please run this command inside a project directory.'
|
||||
'Run this command inside a project directory.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -180,71 +181,54 @@ describe(getProjectAccountNameAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(promptToCreateProjectIfNotExistsAsync, () => {
|
||||
describe(getProjectIdAsync, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(getUserAsync).mockReset();
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockReset();
|
||||
});
|
||||
|
||||
it('returns the existing project', async () => {
|
||||
jest.mocked(getUserAsync).mockImplementation(
|
||||
async (): Promise<Actor> => ({
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
username: 'quin',
|
||||
accounts: [{ id: 'account_id_1', name: 'quin' }],
|
||||
username: 'notnotbrent',
|
||||
accounts: [
|
||||
{ id: 'account_id_1', name: 'notnotbrent' },
|
||||
{ id: 'account_id_2', name: 'dominik' },
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
})
|
||||
);
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockImplementation(async () => 'already-existing-project-id');
|
||||
const exp: any = {};
|
||||
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
|
||||
expect(projectId).toBe('already-existing-project-id');
|
||||
});
|
||||
it('makes a new project if none exists', async () => {
|
||||
jest.mock('../../prompts');
|
||||
jest.mocked(getUserAsync).mockImplementation(
|
||||
async (): Promise<Actor> => ({
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
username: 'quin',
|
||||
accounts: [{ id: 'account_id_1', name: 'quin' }],
|
||||
isExpoAdmin: false,
|
||||
})
|
||||
);
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockImplementation(async () => null);
|
||||
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => 'new-project-id');
|
||||
|
||||
// user agrees to make new project
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => true);
|
||||
const exp: any = {};
|
||||
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
|
||||
expect(projectId).toBe('new-project-id');
|
||||
it('gets the project ID from app config if exists', async () => {
|
||||
await expect(
|
||||
getProjectIdAsync({ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } })
|
||||
).resolves.toEqual('1234');
|
||||
});
|
||||
it('aborts making a new project if the user declines', async () => {
|
||||
jest.mock('../../prompts');
|
||||
jest.mocked(getUserAsync).mockImplementation(
|
||||
async (): Promise<Actor> => ({
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
username: 'quin',
|
||||
accounts: [{ id: 'account_id_1', name: 'quin' }],
|
||||
isExpoAdmin: false,
|
||||
})
|
||||
);
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockImplementation(async () => null);
|
||||
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => 'new-project-id');
|
||||
|
||||
// user doesnt agree to make new project
|
||||
jest.mocked(confirmAsync).mockImplementation(async () => false);
|
||||
const exp: any = {};
|
||||
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
|
||||
expect(projectId).toBe(null);
|
||||
it('fetches the project ID when not in app config, and sets it in the config', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'success',
|
||||
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
|
||||
});
|
||||
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => '2345');
|
||||
|
||||
const projectId = await getProjectIdAsync({ name: 'test', slug: 'test' }, {}, { cwd: '/app' });
|
||||
expect(projectId).toBe('2345');
|
||||
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
|
||||
extra: { eas: { projectId: '2345' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw if writing the ID back to the config fails', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({
|
||||
type: 'fail',
|
||||
config: null,
|
||||
});
|
||||
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => '4567');
|
||||
|
||||
const projectId = await getProjectIdAsync({ name: 'test', slug: 'test' }, {}, { cwd: '/app' });
|
||||
expect(projectId).toBe('4567');
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import terminalLink from 'terminal-link';
|
||||
import { getProjectDashboardUrl } from '../build/utils/url';
|
||||
import { AppPrivacy } from '../graphql/generated';
|
||||
import { AppMutation } from '../graphql/mutations/AppMutation';
|
||||
import { ProjectQuery } from '../graphql/queries/ProjectQuery';
|
||||
import { AppQuery } from '../graphql/queries/AppQuery';
|
||||
import { ora } from '../ora';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
@@ -71,12 +71,13 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
|
||||
* @param slug project slug
|
||||
* @returns A promise resolving to Project ID, null if it doesn't exist
|
||||
*/
|
||||
export async function findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
async function findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
accountName: string,
|
||||
slug: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
return await findProjectIdByAccountNameAndSlugAsync(accountName, slug);
|
||||
const { id } = await AppQuery.byFullNameAsync(`@${accountName}/${slug}`);
|
||||
return id;
|
||||
} catch (err: any) {
|
||||
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
|
||||
throw err;
|
||||
@@ -85,20 +86,6 @@ export async function findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds project by `@accountName/slug` and returns its ID
|
||||
* @param accountName account name
|
||||
* @param slug project slug
|
||||
* @returns A promise resolving to Project ID
|
||||
*/
|
||||
async function findProjectIdByAccountNameAndSlugAsync(
|
||||
accountName: string,
|
||||
slug: string
|
||||
): Promise<string> {
|
||||
const { id } = await ProjectQuery.byUsernameAndSlugAsync(accountName, slug);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers new project on EAS servers
|
||||
* @returns Created project's ID
|
||||
|
||||
@@ -60,7 +60,7 @@ function importMetroConfigFromProject(projectDir: string): typeof MetroConfig {
|
||||
throw new MetroConfigPackageMissingError(
|
||||
'Missing package "metro-config" in the project. ' +
|
||||
'This usually means `react-native` is not installed. ' +
|
||||
'Please verify that dependencies in package.json include "react-native" ' +
|
||||
'Verify that dependencies in package.json include "react-native" ' +
|
||||
'and run `yarn` or `npm install`.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,15 +9,11 @@ import semver from 'semver';
|
||||
|
||||
import { AppPrivacy } from '../graphql/generated';
|
||||
import Log from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { expoCommandAsync } from '../utils/expoCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import {
|
||||
ensureProjectExistsAsync,
|
||||
findProjectIdByAccountNameAndSlugNullableAsync,
|
||||
} from './ensureProjectExists';
|
||||
import { ensureProjectExistsAsync } from './ensureProjectExists';
|
||||
import { getExpoConfig } from './expoConfig';
|
||||
|
||||
export function getProjectAccountName(exp: ExpoConfig, user: Actor): string {
|
||||
@@ -65,7 +61,7 @@ export async function findProjectRootAsync({
|
||||
const projectRootDir = await pkgDir(cwd);
|
||||
if (!projectRootDir) {
|
||||
if (!defaultToProcessCwd) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
throw new Error('Run this command inside a project directory.');
|
||||
} else {
|
||||
return process.cwd();
|
||||
}
|
||||
@@ -83,19 +79,15 @@ export async function findProjectRootAsync({
|
||||
}
|
||||
}
|
||||
|
||||
export async function setProjectIdAsync(
|
||||
/**
|
||||
* Save an EAS project ID to the appropriate field in the app config.
|
||||
*/
|
||||
export async function saveProjectIdToAppConfigAsync(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
options: { env?: Env } = {}
|
||||
): Promise<ExpoConfig | undefined> {
|
||||
): Promise<void> {
|
||||
const exp = getExpoConfig(projectDir, options);
|
||||
|
||||
const privacy = toAppPrivacy(exp.privacy);
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName: getProjectAccountName(exp, await ensureLoggedInAsync()),
|
||||
projectName: exp.slug,
|
||||
privacy,
|
||||
});
|
||||
|
||||
const result = await modifyConfigAsync(projectDir, {
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
});
|
||||
@@ -104,17 +96,24 @@ export async function setProjectIdAsync(
|
||||
case 'success':
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.log();
|
||||
Log.warn('It looks like you are using a dynamic configuration!');
|
||||
Log.log(
|
||||
chalk.dim(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)\n'
|
||||
)
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`Warning: Your project uses dynamic app configuration, and the EAS project ID can't automatically be added to it.`
|
||||
);
|
||||
Log.warn(
|
||||
'In order to finish setting up your project you are going to need manually add the following to your "extra" key:\n\n'
|
||||
chalk.dim(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
|
||||
)
|
||||
);
|
||||
Log.log(chalk.bold(`"extra": {\n ...\n "eas": {\n "projectId": "${projectId}"\n }\n}`));
|
||||
Log.warn();
|
||||
Log.warn(
|
||||
`To complete the setup process, set the ${chalk.bold(
|
||||
'extra.eas.projectId'
|
||||
)} in your ${chalk.bold(getProjectConfigDescription(projectDir))}:`
|
||||
);
|
||||
Log.warn();
|
||||
Log.warn(chalk.bold(JSON.stringify({ expo: { extra: { eas: { projectId } } } }, null, 2)));
|
||||
Log.warn();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
@@ -122,42 +121,57 @@ export async function setProjectIdAsync(
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
|
||||
Log.withTick(`Linked app.json to project with ID ${chalk.bold(projectId)}`);
|
||||
return result.config?.expo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the owner/slug to identify an EAS project on the server.
|
||||
*
|
||||
* @returns the EAS project ID from the server
|
||||
*/
|
||||
export async function fetchProjectIdFromServerAsync(exp: ExpoConfig): Promise<string> {
|
||||
const privacy = toAppPrivacy(exp.privacy);
|
||||
return await ensureProjectExistsAsync({
|
||||
accountName: getProjectAccountName(exp, await ensureLoggedInAsync()),
|
||||
projectName: exp.slug,
|
||||
privacy,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the EAS project ID from the app config. If the project ID is not set in the config.
|
||||
* use the owner/slug to identify an EAS project on the server, and attempt to save the
|
||||
* EAS project ID to the appropriate field in the app config.
|
||||
*/
|
||||
export async function getProjectIdAsync(
|
||||
exp: ExpoConfig,
|
||||
options: { env?: Env } = {}
|
||||
options: { env?: Env } = {},
|
||||
findProjectRootOptions: {
|
||||
cwd?: string;
|
||||
defaultToProcessCwd?: boolean;
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
if (!process.env.EAS_ENABLE_PROJECT_ID) {
|
||||
const privacy = toAppPrivacy(exp.privacy);
|
||||
return await ensureProjectExistsAsync({
|
||||
accountName: getProjectAccountName(exp, await ensureLoggedInAsync()),
|
||||
projectName: exp.slug,
|
||||
privacy,
|
||||
});
|
||||
}
|
||||
|
||||
const localProjectId = exp.extra?.eas?.projectId;
|
||||
if (localProjectId) {
|
||||
return localProjectId;
|
||||
}
|
||||
|
||||
// Set the project ID if it is missing.
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const projectDir = await findProjectRootAsync(findProjectRootOptions);
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
throw new Error('Run this command inside a project directory.');
|
||||
}
|
||||
const newExp = await setProjectIdAsync(projectDir, options);
|
||||
|
||||
const newLocalProjectId = newExp?.extra?.eas?.projectId;
|
||||
if (!newLocalProjectId) {
|
||||
// throw if we still can't locate the projectId
|
||||
throw new Error('Could not retrieve project ID from app.json');
|
||||
const projectId = await fetchProjectIdFromServerAsync(exp);
|
||||
|
||||
try {
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId, options);
|
||||
} catch (e: any) {
|
||||
// saveProjectIdToAppConfigAsync already printed out a set of detailed errors and
|
||||
// instructions on how to fix it. To mimic throwing the error but not halting
|
||||
// execution, just warn here with the error message.
|
||||
Log.warn(e.message);
|
||||
}
|
||||
return newLocalProjectId;
|
||||
|
||||
return projectId;
|
||||
}
|
||||
|
||||
const toAppPrivacy = (privacy: ExpoConfig['privacy']): AppPrivacy => {
|
||||
@@ -196,33 +210,6 @@ export function getProjectConfigDescription(projectDir: string): string {
|
||||
return 'app.config.js/app.json';
|
||||
}
|
||||
|
||||
// return project id of existing/newly created project, or null if user declines
|
||||
export async function promptToCreateProjectIfNotExistsAsync(
|
||||
exp: ExpoConfig
|
||||
): Promise<string | null> {
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const maybeProjectId = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
accountName,
|
||||
exp.slug
|
||||
);
|
||||
if (maybeProjectId) {
|
||||
return maybeProjectId;
|
||||
}
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const shouldCreateProject = await confirmAsync({
|
||||
message: `Looks like ${fullName} is new. Register it with EAS?`,
|
||||
});
|
||||
if (!shouldCreateProject) {
|
||||
return null;
|
||||
}
|
||||
const privacy = toAppPrivacy(exp.privacy);
|
||||
return await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
privacy,
|
||||
});
|
||||
}
|
||||
|
||||
export function isExpoUpdatesInstalled(projectDir: string): boolean {
|
||||
const packageJson = getPackageJson(projectDir);
|
||||
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
|
||||
|
||||
@@ -11,8 +11,9 @@ import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated'
|
||||
import { PublishMutation } from '../graphql/mutations/PublishMutation';
|
||||
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
|
||||
import { PublishQuery } from '../graphql/queries/PublishQuery';
|
||||
import { uploadWithPresignedPostAsync } from '../uploads';
|
||||
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
|
||||
import { expoCommandAsync } from '../utils/expoCli';
|
||||
import chunk from '../utils/expodash/chunk';
|
||||
import uniqBy from '../utils/expodash/uniqBy';
|
||||
|
||||
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
|
||||
@@ -285,12 +286,18 @@ export async function uploadAssetsAsync(
|
||||
const totalAssets = uniqueAssets.length;
|
||||
|
||||
updateSpinnerText?.(totalAssets, totalAssets);
|
||||
|
||||
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(uniqueAssets);
|
||||
const uniqueUploadedAssetCount = missingAssets.length;
|
||||
const { specifications } = await PublishMutation.getUploadURLsAsync(
|
||||
missingAssets.map(ma => ma.contentType)
|
||||
);
|
||||
|
||||
const missingAssetChunks = chunk(missingAssets, 100);
|
||||
const specifications: string[] = [];
|
||||
for (const missingAssets of missingAssetChunks) {
|
||||
const { specifications: chunkSpecifications } = await PublishMutation.getUploadURLsAsync(
|
||||
missingAssets.map(ma => ma.contentType)
|
||||
);
|
||||
specifications.push(...chunkSpecifications);
|
||||
}
|
||||
|
||||
updateSpinnerText?.(totalAssets, missingAssets.length);
|
||||
|
||||
const assetUploadPromiseLimit = promiseLimit(15);
|
||||
@@ -300,7 +307,7 @@ export async function uploadAssetsAsync(
|
||||
missingAssets.map((missingAsset, i) => {
|
||||
assetUploadPromiseLimit(async () => {
|
||||
const presignedPost: PresignedPost = JSON.parse(specifications[i]);
|
||||
await uploadWithPresignedPostAsync(missingAsset.path, presignedPost);
|
||||
await uploadWithPresignedPostWithRetryAsync(missingAsset.path, presignedPost);
|
||||
});
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -164,7 +164,7 @@ function formatServiceAccountSummary({ summary }: ServiceAccountKeyResult): stri
|
||||
value: serviceAccountKeyPath,
|
||||
},
|
||||
{
|
||||
label: 'Account E-mail',
|
||||
label: 'Account Email',
|
||||
value: serviceAccountEmail,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,12 +74,12 @@ function printInstructionsForIosSubmission(submission: SubmissionFragment): void
|
||||
if (submission.status === SubmissionStatus.Finished) {
|
||||
const logMsg = [
|
||||
chalk.bold('Your binary has been successfully uploaded to App Store Connect!'),
|
||||
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.',
|
||||
'- It is now being processed by Apple - you will receive an email when the processing finishes.',
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.',
|
||||
// ascAppIdentifier should be always available for ios submissions but check it anyway
|
||||
submission.iosConfig?.ascAppIdentifier &&
|
||||
`- When it's done, you can see your build here: ${link(
|
||||
`https://appstoreconnect.apple.com/apps/${submission.iosConfig?.ascAppIdentifier}/appstore/ios`
|
||||
`https://appstoreconnect.apple.com/apps/${submission.iosConfig?.ascAppIdentifier}/testflight/ios`
|
||||
)}`,
|
||||
].join('\n');
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -19,11 +19,12 @@ enum SubmissionErrorCode {
|
||||
IOS_IPAD_INVALID_ORIENTATION = 'SUBMISSION_SERVICE_IOS_IPAD_INVALID_ORIENTATION',
|
||||
IOS_APPLE_MAINTENANCE = 'SUBMISSION_SERVICE_IOS_APPLE_MAINTENANCE',
|
||||
IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE',
|
||||
IOS_SIGNED_WITH_ADHOC_PROFILE = 'SUBMISSION_SERVICE_IOS_SIGNED_WITH_ADHOC_PROFILE',
|
||||
}
|
||||
|
||||
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
[SubmissionErrorCode.ARCHIVE_DOWNLOAD_NOT_FOUND_ERROR]:
|
||||
"Failed to download the archive file (Response code: 404 Not Found). Please make sure the URL you've provided is correct.",
|
||||
"Failed to download the archive file (Response code: 404 Not Found). Make sure the URL you've provided is correct.",
|
||||
[SubmissionErrorCode.ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR]:
|
||||
'Failed to download the archive file (Response code: 403 Forbidden). This is most probably caused by trying to upload an expired build artifact. All EAS build artifacts expire after 30 days.',
|
||||
[SubmissionErrorCode.ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR]:
|
||||
@@ -32,7 +33,7 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
"EAS Submit couldn't find a valid build artifact within provided compressed archive.\n" +
|
||||
'If you provide a tar.gz archive, it should contain at least one .apk/.aab/.ipa file, depending on the submission platform.',
|
||||
[SubmissionErrorCode.ANDROID_UNKNOWN_ERROR]:
|
||||
"We couldn't figure out what went wrong. Please see logs to learn more.",
|
||||
"We couldn't figure out what went wrong. See logs to learn more.",
|
||||
[SubmissionErrorCode.ANDROID_FIRST_UPLOAD_ERROR]:
|
||||
"You haven't submitted this app to Google Play Store yet. The first submission of the app needs to be performed manually.\n" +
|
||||
`${learnMore('https://expo.fyi/first-android-submission')}.`,
|
||||
@@ -50,29 +51,31 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
"If you're submitting an Expo project built with EAS Build, increment the build number in app.json and build the project again.\n" +
|
||||
`${learnMore('https://expo.fyi/bumping-ios-build-number')}.`,
|
||||
[SubmissionErrorCode.IOS_UNKNOWN_ERROR]:
|
||||
"We couldn't figure out what went wrong. Please see logs to learn more.",
|
||||
"We couldn't figure out what went wrong. See logs to learn more.",
|
||||
[SubmissionErrorCode.IOS_MISSING_APP_ICON]:
|
||||
'Your iOS app icon is missing or is an invalid format. The icon must be a 1024x1024 PNG image with no transparency.\n' +
|
||||
'Please check your icon image and icon configuration in app.json.\n' +
|
||||
'Check your icon image and icon configuration in app.json.\n' +
|
||||
`${learnMore('https://docs.expo.dev/guides/app-icons/')}`,
|
||||
[SubmissionErrorCode.IOS_INVALID_SIGNATURE]:
|
||||
'Your app signature seems to be invalid.\n' +
|
||||
"Please check your iOS Distribution Certificate and your app's Provisioning Profile.\n" +
|
||||
"Check your iOS Distribution Certificate and your app's Provisioning Profile.\n" +
|
||||
`${learnMore('https://docs.expo.dev/distribution/app-signing')}`,
|
||||
[SubmissionErrorCode.IOS_INCORRECT_CREDENTIALS]:
|
||||
'Your Apple ID or app-specific password is incorrect. Please verify that you entered them correctly and try again.',
|
||||
'Your Apple ID or app-specific password is incorrect. Verify that you entered them correctly and try again.',
|
||||
[SubmissionErrorCode.IOS_IPAD_INVALID_ORIENTATION]:
|
||||
"Your app doesn't support iPad multitasking and has to require full screen.\n" +
|
||||
"If you're submitting a managed Expo project, set the `expo.ios.requireFullScreen` to true in app.json and build the project again.\n" +
|
||||
`${learnMore('https://expo.fyi/ipad-requires-fullscreen')}`,
|
||||
[SubmissionErrorCode.IOS_APPLE_MAINTENANCE]:
|
||||
'It looks like Apple servers are undergoing an unscheduled maintenance. Please try again later.',
|
||||
'It looks like Apple servers are undergoing an unscheduled maintenance. Try again later.',
|
||||
[SubmissionErrorCode.IOS_INVALID_PROVISIONING_PROFILE_SIGNATURE]:
|
||||
'Invalid Provisioning Profile Signature (ITMS-90165)\n' +
|
||||
"Some of Apple's certificates have expired.\n" +
|
||||
'Please delete your Provisioning Profile from your account. Then rebuild the app interactively to generate a new one, and try submitting it to the App Store again.',
|
||||
'Delete your Provisioning Profile from your account. Then rebuild the app interactively to generate a new one, and try submitting it to the App Store again.',
|
||||
[SubmissionErrorCode.UPLOAD_TAKING_TOO_LONG_ERROR]:
|
||||
'Submission has reached the timeout limit. Please try again.',
|
||||
'Submission has reached the timeout limit. Try again.',
|
||||
[SubmissionErrorCode.IOS_SIGNED_WITH_ADHOC_PROFILE]:
|
||||
'Invalid Provisioning Profile for Apple App Store distribution. The application was signed with an Ad Hoc/Enterprise Provisioning Profile, which is meant for "Internal Distribution". In order to distribute an app on the store, it must be signed with a Distribution Provisioning Profile. Ensure that you are submitting the correct build, or rebuild the application with a Distribution Provisioning Profile and submit that new build.',
|
||||
};
|
||||
|
||||
export function printSubmissionError(error: SubmissionError): boolean {
|
||||
|
||||
@@ -11,7 +11,6 @@ const APP_STORE_NAMES: Record<AppPlatform, string> = {
|
||||
[AppPlatform.Ios]: 'Apple App Store Connect',
|
||||
};
|
||||
|
||||
const CHECK_TIMEOUT_MS = 3600_000;
|
||||
const CHECK_INTERVAL_MS = 5_000;
|
||||
|
||||
export async function waitForSubmissionsEndAsync(
|
||||
@@ -25,9 +24,7 @@ export async function waitForSubmissionsEndAsync(
|
||||
|
||||
const spinner = ora(`Submitting`).start();
|
||||
|
||||
let time = new Date().getTime();
|
||||
const timeoutTime = time + CHECK_TIMEOUT_MS;
|
||||
while (time <= timeoutTime) {
|
||||
while (true) {
|
||||
const submissions = await Promise.all(
|
||||
initialSubmissions.map(({ id }) => {
|
||||
try {
|
||||
@@ -76,11 +73,8 @@ export async function waitForSubmissionsEndAsync(
|
||||
}
|
||||
}
|
||||
|
||||
time = new Date().getTime();
|
||||
await sleepAsync(CHECK_INTERVAL_MS);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error('Timeout reached! It is taking longer than expected to complete, aborting...');
|
||||
}
|
||||
|
||||
function getSingleSpinnerText(submission: SubmissionFragment): string {
|
||||
|
||||
@@ -83,7 +83,7 @@ export function ensureValidVersions(exp: ExpoConfig, platform: RequestedPlatform
|
||||
'sdkVersion'
|
||||
)} to configure ${chalk.bold(
|
||||
'expo-updates'
|
||||
)}. Please specify at least one of these properties under the ${chalk.bold(
|
||||
)}. Specify at least one of these properties under the ${chalk.bold(
|
||||
'expo'
|
||||
)} key in ${chalk.bold('app.json')}. ${learnMore(
|
||||
'https://docs.expo.dev/eas-update/runtime-versions/'
|
||||
|
||||
@@ -3,6 +3,7 @@ import FormData from 'form-data';
|
||||
import fs from 'fs-extra';
|
||||
import { Response } from 'node-fetch';
|
||||
import nullthrows from 'nullthrows';
|
||||
import promiseRetry from 'promise-retry';
|
||||
import { URL } from 'url';
|
||||
|
||||
import fetch from './fetch';
|
||||
@@ -18,7 +19,11 @@ export async function uploadFileAtPathToS3Async(
|
||||
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(type);
|
||||
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
|
||||
|
||||
const response = await uploadWithPresignedPostAsync(path, presignedPost, handleProgressEvent);
|
||||
const response = await uploadWithPresignedPostWithProgressAsync(
|
||||
path,
|
||||
presignedPost,
|
||||
handleProgressEvent
|
||||
);
|
||||
const location = nullthrows(
|
||||
response.headers.get('location'),
|
||||
`location does not exist in response headers (make sure you're uploading to AWS S3)`
|
||||
@@ -27,11 +32,48 @@ export async function uploadFileAtPathToS3Async(
|
||||
return { url, bucketKey: presignedPost.fields.key };
|
||||
}
|
||||
|
||||
export async function uploadWithPresignedPostAsync(
|
||||
export async function uploadWithPresignedPostWithRetryAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost,
|
||||
handleProgressEvent?: ProgressHandler
|
||||
presignedPost: PresignedPost
|
||||
): Promise<Response> {
|
||||
return await promiseRetry(
|
||||
async retry => {
|
||||
// retry fetch errors (usually connection or DNS errors)
|
||||
let response: Response;
|
||||
try {
|
||||
response = await uploadWithPresignedPostAsync(file, presignedPost);
|
||||
} catch (e: any) {
|
||||
return retry(e);
|
||||
}
|
||||
|
||||
// retry 408, 429, 5xx as suggested by google
|
||||
if (
|
||||
response.status === 408 ||
|
||||
response.status === 429 ||
|
||||
(response.status >= 500 && response.status <= 599)
|
||||
) {
|
||||
return retry(new Error(`Presigned upload responded with a ${response.status} status`));
|
||||
}
|
||||
|
||||
// don't retry other errors
|
||||
if (!response.ok) {
|
||||
throw new Error(`Presigned upload responded with a ${response.status} status`);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
// retry parameters match google suggested defaults: https://cloud.google.com/storage/docs/retry-strategy#node.js
|
||||
{
|
||||
retries: 3,
|
||||
factor: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createPresignedPostFormDataAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost
|
||||
): Promise<{ form: FormData; fileSize: number }> {
|
||||
const fileStat = await fs.stat(file);
|
||||
const fileSize = fileStat.size;
|
||||
const form = new FormData();
|
||||
@@ -39,6 +81,30 @@ export async function uploadWithPresignedPostAsync(
|
||||
form.append(fieldKey, fieldValue);
|
||||
}
|
||||
form.append('file', fs.createReadStream(file), { knownLength: fileSize });
|
||||
return { form, fileSize };
|
||||
}
|
||||
|
||||
async function uploadWithPresignedPostAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost
|
||||
): Promise<Response> {
|
||||
const { form } = await createPresignedPostFormDataAsync(file, presignedPost);
|
||||
const formHeaders = form.getHeaders();
|
||||
return await fetch(presignedPost.url, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
...formHeaders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadWithPresignedPostWithProgressAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost,
|
||||
handleProgressEvent: ProgressHandler
|
||||
): Promise<Response> {
|
||||
const { form, fileSize } = await createPresignedPostFormDataAsync(file, presignedPost);
|
||||
const formHeaders = form.getHeaders();
|
||||
const uploadPromise = fetch(presignedPost.url, {
|
||||
method: 'POST',
|
||||
@@ -49,27 +115,23 @@ export async function uploadWithPresignedPostAsync(
|
||||
});
|
||||
|
||||
let currentSize = 0;
|
||||
if (handleProgressEvent) {
|
||||
form.addListener('data', (chunk: Buffer) => {
|
||||
currentSize += Buffer.byteLength(chunk);
|
||||
handleProgressEvent({
|
||||
progress: {
|
||||
total: fileSize,
|
||||
percent: currentSize / fileSize,
|
||||
transferred: currentSize,
|
||||
},
|
||||
});
|
||||
form.addListener('data', (chunk: Buffer) => {
|
||||
currentSize += Buffer.byteLength(chunk);
|
||||
handleProgressEvent({
|
||||
progress: {
|
||||
total: fileSize,
|
||||
percent: currentSize / fileSize,
|
||||
transferred: currentSize,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await uploadPromise;
|
||||
handleProgressEvent({ isComplete: true });
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
handleProgressEvent({ isComplete: true, error });
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
return await uploadPromise;
|
||||
});
|
||||
try {
|
||||
const response = await uploadPromise;
|
||||
handleProgressEvent({ isComplete: true });
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
handleProgressEvent({ isComplete: true, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
StatuspageServiceFragment,
|
||||
StatuspageServiceName,
|
||||
StatuspageServiceStatus,
|
||||
} from '../graphql/generated';
|
||||
import { StatuspageServiceQuery } from '../graphql/queries/StatuspageServiceQuery';
|
||||
import Log, { link } from '../log';
|
||||
|
||||
export async function maybeWarnAboutEasOutagesAsync(
|
||||
serviceNames: StatuspageServiceName[]
|
||||
): Promise<void> {
|
||||
const services = await getStatuspageServiceAsync(serviceNames);
|
||||
|
||||
for (const service of services) {
|
||||
warnAboutServiceOutage(service);
|
||||
}
|
||||
}
|
||||
|
||||
const humanReadableServiceName: Record<StatuspageServiceName, string> = {
|
||||
[StatuspageServiceName.EasBuild]: 'EAS Build',
|
||||
[StatuspageServiceName.EasSubmit]: 'EAS Submit',
|
||||
[StatuspageServiceName.EasUpdate]: 'EAS Update',
|
||||
};
|
||||
|
||||
function warnAboutServiceOutage(service: StatuspageServiceFragment): void {
|
||||
if (service.status === StatuspageServiceStatus.Operational) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outageType = service.status === StatuspageServiceStatus.MajorOutage ? 'major' : 'partial';
|
||||
|
||||
Log.warn(
|
||||
chalk.bold(`${humanReadableServiceName[service.name]} is experiencing a ${outageType} outage.`)
|
||||
);
|
||||
|
||||
if (service.incidents.length > 0) {
|
||||
const [currentIncident] = service.incidents;
|
||||
Log.warn(`Reason: ${currentIncident.name}`);
|
||||
}
|
||||
|
||||
Log.warn(
|
||||
`All information on service status and incidents available at ${link(
|
||||
'https://status.expo.dev/'
|
||||
)}.`
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
async function getStatuspageServiceAsync(
|
||||
serviceNames: StatuspageServiceName[]
|
||||
): Promise<StatuspageServiceFragment[]> {
|
||||
try {
|
||||
return await StatuspageServiceQuery.statuspageServicesAsync(serviceNames);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -84,4 +84,17 @@ describe(Ignore, () => {
|
||||
await ignore.initIgnoreAsync();
|
||||
expect(ignore.ignores('.git')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not throw an error if there is a trailing backslash in the gitignore', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'.gitignore': 'dir\\',
|
||||
},
|
||||
'/root'
|
||||
);
|
||||
|
||||
const ignore = new Ignore('/root');
|
||||
await ignore.initIgnoreAsync();
|
||||
expect(() => ignore.ignores('dir/test')).not.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ export class Ignore {
|
||||
if (await fs.pathExists(easIgnorePath)) {
|
||||
this.ignoreMapping = [
|
||||
['', createIgnore().add(DEFAULT_IGNORE)],
|
||||
['', createIgnore().add(await fs.readFile(easIgnorePath, 'utf8'))],
|
||||
['', createIgnore().add(await this.readIgnoreFileAsync(easIgnorePath))],
|
||||
];
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export class Ignore {
|
||||
ignoreFilePaths.map(async filePath => {
|
||||
return [
|
||||
filePath.slice(0, filePath.length - GITIGNORE_FILENAME.length),
|
||||
createIgnore().add(await fs.readFile(path.join(this.rootDir, filePath), 'utf8')),
|
||||
createIgnore().add(await this.readIgnoreFileAsync(path.join(this.rootDir, filePath))),
|
||||
] as const;
|
||||
})
|
||||
);
|
||||
@@ -72,6 +72,16 @@ export class Ignore {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async readIgnoreFileAsync(filePath: string): Promise<string> {
|
||||
const fileContents = await fs.readFile(filePath, 'utf-8');
|
||||
const lines = fileContents.split('\n');
|
||||
// Strip trailing '\'. This logic can be removed after fix upstream is released.
|
||||
// https://github.com/kaelzhang/node-ignore/issues/81
|
||||
return lines
|
||||
.map((line: string) => (line.slice(-1) === '\\' ? line.slice(0, -1) : line))
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeShallowCopyAsync(src: string, dst: string): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "0.60.0",
|
||||
"version": "1.2.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
|
||||
@@ -24,24 +24,24 @@
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The compatible versions of EAS CLI with this config",
|
||||
"examples": [
|
||||
">=0.54.1"
|
||||
]
|
||||
"examples": [">=0.54.1"]
|
||||
},
|
||||
"requireCommit": {
|
||||
"type": "boolean",
|
||||
"description": "If all changes required to be committed before building or submitting"
|
||||
},
|
||||
"appVersionSource": {
|
||||
"enum": [
|
||||
"local",
|
||||
"remote"
|
||||
],
|
||||
"enum": ["local", "remote"],
|
||||
"markdownDescription": "Version policy defines whether version of your app should be based on your local project or values stored on EAS servers (remote).\n\nThis is the configuration required for `eas build:version:set` and works with the `autoIncrement` options per platform.",
|
||||
"markdownEnumDescriptions": [
|
||||
"When using local, the `autoIncrement` is based on your local project values.",
|
||||
"When using remote, the `autoIncrement` is based on the values stored on EAS servers."
|
||||
]
|
||||
},
|
||||
"promptToConfigurePushNotfications" : {
|
||||
"type": "boolean",
|
||||
"description": "Specifies where EAS CLI should prompt to configure Push Notifications credentials. Defaults to true.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -91,10 +91,7 @@
|
||||
"description": "The name of the build profile that the current one should inherit values from. This value can't be specified per platform."
|
||||
},
|
||||
"credentialsSource": {
|
||||
"enum": [
|
||||
"remote",
|
||||
"local"
|
||||
],
|
||||
"enum": ["remote", "local"],
|
||||
"description": "The source of credentials used to sign build artifacts. Learn more: https://docs.expo.dev/app-signing/local-credentials/",
|
||||
"markdownDescription": "The source of credentials used to sign build artifacts.\n\n- `remote` - if you want to use the credentials managed by EAS.\n- `local` - if you want to provide your own `credentials.json` file. [learn more](https://docs.expo.dev/app-signing/local-credentials/)",
|
||||
"default": "remote",
|
||||
@@ -116,10 +113,7 @@
|
||||
"markdownDescription": "The channel is a name we can give to multiple builds to identify them easily. [Learn more](https://docs.expo.dev/eas-update/how-eas-update-works/)\n\n**This field only applies to the EAS Update service**, if your project still uses Classic Updates then use the [releaseChannel](https://docs.expo.dev/build-reference/eas-json/#releasechannel) field instead."
|
||||
},
|
||||
"distribution": {
|
||||
"enum": [
|
||||
"store",
|
||||
"internal"
|
||||
],
|
||||
"enum": ["store", "internal"],
|
||||
"description": "The method of distributing your app. Learn more: https://docs.expo.dev/build/internal-distribution/",
|
||||
"markdownDescription": "The method of distributing your app.\n\n- `internal` - with this option you'll be able to share your build URLs with anyone, and they will be able to install the builds to their devices straight from the Expo website. When using `internal`, make sure the build produces an APK or IPA file. Otherwise, the shareable URL will be useless. [Learn more](https://docs.expo.dev/build/internal-distribution/)\n- `store` - produces builds for store uploads, your build URLs won't be shareable.",
|
||||
"markdownEnumDescriptions": [
|
||||
@@ -168,6 +162,12 @@
|
||||
"description": "The cache configuration.",
|
||||
"markdownDescription": "Cache configuration. This feature is intended for caching values that require a lot of computation, e.g. compilation results (both final binaries and any intermediate files), but it wouldn't work well for `node_modules` because the cache is not local to the machine, so the download speed is similar to downloading from the npm registry."
|
||||
},
|
||||
"autoIncrement": {
|
||||
"type": "boolean",
|
||||
"description": "Controls how EAS CLI bumps your application build version.",
|
||||
"markdownDescription": "When enabled, for iOS, bumps the last component of `expo.ios.buildNumber` (e.g. `1.2.3.39` -> `1.2.3.40`) and for Android, bumps `expo.android.versionCode` (e.g. `3` -> `4`).",
|
||||
"default": false
|
||||
},
|
||||
"android": {
|
||||
"$ref": "#/definitions/BuildProfileAndroid"
|
||||
},
|
||||
@@ -219,20 +219,15 @@
|
||||
"description": "The version of Android NDK."
|
||||
},
|
||||
"autoIncrement": {
|
||||
"enum": [
|
||||
false,
|
||||
true,
|
||||
"versionCode",
|
||||
"version"
|
||||
],
|
||||
"enum": [false, true, "versionCode", "version"],
|
||||
"description": "Controls how EAS CLI bumps your application build version.",
|
||||
"markdownDescription": "Controls how EAS CLI bumps your application build version.\n\nAllowed values:\n- `\"version\"` - the patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`).\n- `\"versionCode\"` (or `true`) - `expo.android.versionCode` is bumped (e.g. `3` -> `4`).\n- `false` - versions won't be bumped automatically (default)\n\nIn the case of a bare project, it also updates versions in native code. `expo.version` corresponds to `versionName` and `expo.android.versionCode` to `versionCode` in the `build.gradle`. Google Play uses these values to identify the app build, `versionName` is the version visible to users, whereas `versionCode` defines the version number. The combination of those needs to be unique, so you can bump either of them.\n\nThis feature is not intended for use with dynamic configuration (app.config.js). EAS CLI will throw an error if you don't use app.json.",
|
||||
"markdownDescription": "Controls how EAS CLI bumps your application build version.\n\nAllowed values:\n- `\"version\"` - bumps the patch of `expo.version` (e.g. `1.2.3` -> `1.2.4`).\n- `\"versionCode\"` (or `true`) - bumps `expo.android.versionCode` (e.g. `3` -> `4`).\n- `false` - versions won't be bumped automatically (default)\n\nIn the case of a bare project, it also updates versions in native code. `expo.version` corresponds to `versionName` and `expo.android.versionCode` to `versionCode` in the `build.gradle`. Google Play uses these values to identify the app build, `versionName` is the version visible to users, whereas `versionCode` defines the version number. The combination of those needs to be unique, so you can bump either of them.\n\nThis feature is not intended for use with dynamic configuration (app.config.js). EAS CLI will throw an error if you don't use app.json.",
|
||||
"default": false,
|
||||
"markdownEnumDescriptions": [
|
||||
"Versions won't be bumped automatically (default).",
|
||||
"`expo.android.versionCode` is bumped (e.g. `3` -> `4`).",
|
||||
"`expo.android.versionCode` is bumped (e.g. `3` -> `4`).",
|
||||
"The patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`)."
|
||||
"Bumps `expo.android.versionCode` (e.g. `3` -> `4`).",
|
||||
"Bumps `expo.android.versionCode` (e.g. `3` -> `4`).",
|
||||
"Bumps the patch of `expo.version` (e.g. `1.2.3` -> `1.2.4`)."
|
||||
]
|
||||
},
|
||||
"buildType": {
|
||||
@@ -274,20 +269,15 @@
|
||||
]
|
||||
},
|
||||
"autoIncrement": {
|
||||
"enum": [
|
||||
false,
|
||||
true,
|
||||
"buildNumber",
|
||||
"version"
|
||||
],
|
||||
"enum": [false, true, "buildNumber", "version"],
|
||||
"description": "Controls how EAS CLI bumps your application build version.",
|
||||
"markdownDescription": "Controls how EAS CLI bumps your application build version.\n\nAllowed values:\n\n- `\"version\"` - the patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`).\n- `\"buildNumber\"` (or `true`) - the last component of `expo.ios.buildNumber` is bumped (e.g. `1.2.3.39` -> `1.2.3.40`).\n- `false` - versions won't be bumped automatically (default)\n\nIn the case of a bare project, it also updates versions in native code. `expo.version` corresponds to `CFBundleShortVersionString` and `expo.ios.buildNumber` to `CFBundleVersion` in the `Info.plist`. The App Store is using those values to identify the app build, `CFBundleShortVersionString` is the version visible to users, whereas `CFBundleVersion` defines the build number. The combination of those needs to be unique, so you can bump either of them.\n\nThis feature is not intended for use with dynamic configuration (app.config.js). EAS CLI will throw an error if you don't use app.json.",
|
||||
"markdownDescription": "Controls how EAS CLI bumps your application build version.\n\nAllowed values:\n\n- `\"version\"` - bumps the patch of `expo.version` (e.g. `1.2.3` -> `1.2.4`).\n- `\"buildNumber\"` (or `true`) - bumps the last component of `expo.ios.buildNumber` (e.g. `1.2.3.39` -> `1.2.3.40`).\n- `false` - versions won't be bumped automatically (default)\n\nIn the case of a bare project, it also updates versions in native code. `expo.version` corresponds to `CFBundleShortVersionString` and `expo.ios.buildNumber` to `CFBundleVersion` in the `Info.plist`. The App Store is using those values to identify the app build, `CFBundleShortVersionString` is the version visible to users, whereas `CFBundleVersion` defines the build number. The combination of those needs to be unique, so you can bump either of them.\n\nThis feature is not intended for use with dynamic configuration (app.config.js). EAS CLI will throw an error if you don't use app.json.",
|
||||
"default": false,
|
||||
"markdownEnumDescriptions": [
|
||||
"Versions won't be bumped automatically (default)",
|
||||
"The last component of `expo.ios.buildNumber` is bumped (e.g. `1.2.3.39` -> `1.2.3.40`)",
|
||||
"The last component of `expo.ios.buildNumber` is bumped (e.g. `1.2.3.39` -> `1.2.3.40`)",
|
||||
"The patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`)"
|
||||
"Bumps the last component of `expo.ios.buildNumber` (e.g. `1.2.3.39` -> `1.2.3.40`)",
|
||||
"Bumps the last component of `expo.ios.buildNumber` (e.g. `1.2.3.39` -> `1.2.3.40`)",
|
||||
"Bumps the patch of `expo.version` (e.g. `1.2.3` -> `1.2.4`)"
|
||||
]
|
||||
},
|
||||
"image": {
|
||||
|
||||
@@ -63,6 +63,31 @@ test('valid eas.json for development client builds', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test(`valid eas.json with autoIncrement flag at build profile root`, async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
autoIncrement: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'production');
|
||||
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
expect(androidProfile).toEqual({
|
||||
autoIncrement: true,
|
||||
credentialsSource: 'remote',
|
||||
distribution: 'store',
|
||||
});
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
autoIncrement: true,
|
||||
credentialsSource: 'remote',
|
||||
distribution: 'store',
|
||||
});
|
||||
});
|
||||
|
||||
test('valid profile for internal distribution on Android', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
|
||||
@@ -21,7 +21,10 @@ export function resolveBuildProfile<T extends Platform>({
|
||||
profileName: profileName ?? 'production',
|
||||
});
|
||||
const { android, ios, ...base } = easJsonProfile;
|
||||
const withoutDefaults = mergeProfiles(base, easJsonProfile[platform] ?? {});
|
||||
const withoutDefaults = mergeProfiles(
|
||||
base,
|
||||
(easJsonProfile[platform] as EasJsonBuildProfileResolved) ?? {}
|
||||
);
|
||||
return mergeProfiles(getDefaultProfile(platform), withoutDefaults) as BuildProfile<T>;
|
||||
}
|
||||
|
||||
@@ -79,10 +82,16 @@ function mergeProfiles(
|
||||
};
|
||||
}
|
||||
if (base.android && update.android) {
|
||||
result.android = mergeProfiles(base.android, update.android);
|
||||
result.android = mergeProfiles(
|
||||
base.android as EasJsonBuildProfileResolved,
|
||||
update.android as EasJsonBuildProfileResolved
|
||||
);
|
||||
}
|
||||
if (base.ios && update.ios) {
|
||||
result.ios = mergeProfiles(base.ios, update.ios);
|
||||
result.ios = mergeProfiles(
|
||||
base.ios as EasJsonBuildProfileResolved,
|
||||
update.ios as EasJsonBuildProfileResolved
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const CommonBuildProfileSchema = Joi.object({
|
||||
yarn: Joi.string().empty(null).custom(semverCheck),
|
||||
expoCli: Joi.string().empty(null).custom(semverCheck),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
|
||||
autoIncrement: Joi.alternatives().try(Joi.boolean()),
|
||||
});
|
||||
|
||||
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface CommonBuildProfile {
|
||||
channel?: string;
|
||||
developmentClient?: boolean;
|
||||
prebuildCommand?: string;
|
||||
autoIncrement?: boolean;
|
||||
|
||||
node?: string;
|
||||
yarn?: string;
|
||||
@@ -28,7 +29,7 @@ export interface CommonBuildProfile {
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface AndroidBuildProfile extends CommonBuildProfile {
|
||||
export interface AndroidBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
|
||||
withoutCredentials?: boolean;
|
||||
image?: Android.BuilderEnvironment['image'];
|
||||
ndk?: string;
|
||||
@@ -40,7 +41,7 @@ export interface AndroidBuildProfile extends CommonBuildProfile {
|
||||
artifactPath?: string;
|
||||
}
|
||||
|
||||
export interface IosBuildProfile extends CommonBuildProfile {
|
||||
export interface IosBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
|
||||
enterpriseProvisioning?: IosEnterpriseProvisioning;
|
||||
autoIncrement?: IosVersionAutoIncrement;
|
||||
simulator?: boolean;
|
||||
|
||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user