Compare commits

...
58 Commits
Author SHA1 Message Date
Quinlan Jung a5dc0bd27c v5.2.0 2023-09-05 13:50:41 -07:00
Quinlan Jung 54127c4e37 [ENG-9828][eas-cli] rollouts: view action for CI (#2040)
* [eas-cli] rollouts: non-interactive view

* update CHANGELOG.md

* rebase
2023-09-05 13:49:12 -07:00
Quinlan Jung 9097ab2537 [ENG-9602][eas-cli] rollouts: json output for ci (#2037)
* [eas-cli] rollouts: json output for ci

* update CHANGELOG.md

* fix tests
2023-09-05 13:01:20 -07:00
Quinlan Jung 4e2029a5c5 [eas-cli] rollout tests (#2042)
* [eas-cli] rollout tests

* update CHANGELOG.md
2023-09-05 12:30:31 -07:00
Quinlan Jung ad02a056d7 [eas-cli] remove unreachable codesigning option (#2041)
* [eas-cli] remove unreachable option

* update CHANGELOG.md
2023-09-05 12:29:23 -07:00
Quinlan Jung ea9f734b11 [eas-cli] fix graphql (#2039)
* [eas-cli] fix graphql

* update CHANGELOG.md
2023-09-05 12:27:54 -07:00
Piotr Szeremeta 7db9d79c55 [Fix] Remove doubled definition of pnpm field from eas.schema.json (#2038) 2023-09-04 07:44:39 +02:00
Quinlan Jung 2e31b42b76 [eas-cli] only print channel-branch pairing if we created a channel (#2036)
* [eas-cli] only print channel-branch pairing if we created a channel

* update CHANGELOG.md
2023-09-01 12:38:42 -07:00
Expo CI 66d02d9724 update CHANGELOG.md 2023-09-01 14:14:32 +00:00
Piotr Szeremeta 7c96cc10cf v5.1.0 2023-09-01 16:05:53 +02:00
Piotr Szeremeta 523a781ba5 upgrade eas-cli-local-build-plugin to 1.0.42 2023-09-01 16:05:39 +02:00
Piotr Szeremeta 6cf8bd5c5a [ENG-9473] Support installing selected pnpm version (#1988)
* [wip][ENG-9473]

* [ENG-9473] Add `pnpm` option to `builderEnvironment`.

* update CHANGELOG.md

* Fix changelog
2023-09-01 15:26:28 +02:00
dependabot[bot] 67e9ea706b Bump word-wrap from 1.2.3 to 1.2.5 (#2033)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 17:04:07 +02:00
Szymon Dziedzic c22f410889 [eas-cli][eas-json] bump semver version (#2030)
* [eas-cli][eas-json] bump `semver` version

* update scripts yarn.lock
2023-08-30 16:00:49 +02:00
Radosław Krzemień 9ff47af4fa [eas-cli] [ENG-7536] Stop app config error repeating (#2020)
* [eas-cli] Stop app config error repeating

If app config error is thrown it is re-thrown with modified message instead of printing it to the as a "cryptic error" and repeating with 0% chances of success (since the config is incorrect)

See: https://linear.app/expo/issue/ENG-7536/make-app-config-error-on-credentials-command-not-repeat-indefinitely

* [eas-cli] Create custom error

Replaced generic error with specific message with a custom error class, which allows for not relying on a message to be exactly as expected

See: https://linear.app/expo/issue/ENG-7536/make-app-config-error-on-credentials-command-not-repeat-indefinitely

* update CHANGELOG.md

* [eas-cli] Remove blank line

Removed blank line from changelog

See: https://linear.app/expo/issue/ENG-7536/make-app-config-error-on-credentials-command-not-repeat-indefinitely
2023-08-30 09:11:00 +02:00
Quinlan Jung 324dce8fc0 [eas-cli] fix printing bug: branch with no update (#2023)
* [eas-cli] fix branch with no update printing bug

* tests

* update CHANGELOG.md
2023-08-29 14:07:35 -07:00
Expo CI b30c86f0c0 update CHANGELOG.md 2023-08-29 11:51:19 +00:00
Szymon Dziedzic 008279e1dd v5.0.2 2023-08-29 13:15:39 +02:00
Cedric van Putten d00814f84b fix(eas-cli): implement variadic --platform flag when creating EAS updates (#1939)
* fix(eas-cli): implement variadic `--platform` flag when creating EAS updates

* fix(eas-cli): finish todo and set minimum `@expo/[email protected]` version for multiple `--platform` flags during export

* update CHANGELOG.md

* fix(eas-cli): be verbose about `--platform=<flag>` in bundle script
2023-08-29 13:11:49 +02:00
Expo CI 81aa127e28 update CHANGELOG.md 2023-08-28 19:05:12 +00:00
Szymon Dziedzic d321a0bccb v5.0.1 2023-08-28 20:54:10 +02:00
Szymon Dziedzic a1d86549ed [eas-cli] fix eas update passing platform argument incorrectly to expo-cli (#2028)
* [eas-cli] fix `eas update` passing `platform` argument incorrectly to `expo-cli`

* update CHANGELOG.md
2023-08-28 20:51:18 +02:00
Expo CI f7e9af913c update CHANGELOG.md 2023-08-28 14:00:12 +00:00
Szymon Dziedzic 137f9d4ac7 v5.0.0 2023-08-28 14:20:36 +02:00
Szymon Dziedzic 2ac863a9e3 upgrade eas-cli-local-build-plugin to 1.0.39 2023-08-28 14:19:20 +02:00
Quinlan Jung 11be9eba49 [ENG-9685][eas-cli] create a linked channel on build if not exists (#2017)
* [eas-cli] create linked channel on build

* refactor

* update CHANGELOG.md

* amend doc block
2023-08-25 12:07:59 -07:00
Will Schurman b36db15601 [eas-cli][updates] configure updates as well when somebody tries to run a build with channel set (#2016) 2023-08-25 09:14:29 -07:00
Piotr Szeremeta 93c5dc1a32 [ENG-9422] nvmrc support for setting node version (#1998)
* [ENG-9422][eas-cli] Use .nvmrc to set node version

* update CHANGELOG.md
2023-08-25 15:46:55 +02:00
Szymon Dziedzic c86550c0c8 [eas-cli][eas-json] add developmentClient to metadata (#2015)
* [eas-cli][eas-json] add `developmentClient` to metadata

* update CHANGELOG.md
2023-08-25 13:26:33 +02:00
Brent Vatne 36b4296d3c [.github] Add issue template config 2023-08-24 17:09:47 -07:00
Brent Vatne 8c08541ca3 Create SECURITY.md 2023-08-24 17:06:03 -07:00
Will Schurman 5a1406d39c [eas-cli] Add info to EAS Update asset upload process about asset counts and limits (#2013) 2023-08-24 09:22:30 -07:00
Radosław Krzemień 78ea88cc5f [eas-cli] [ENG-7591] Detect not provisioned devices (#2011)
* [eas-cli] Detect not provisioned devices

After selecting devices to be provisioned and making a call to App Store Connect API the result is compared with the list of chosen devices. If any are missing, a message is displayed to the user

See: https://linear.app/expo/issue/ENG-7591/detect-processing-devices-on-apple-developer-portal-and-dont-allow-the

* [eas-cli] Adjust message

Updated the message with the link to the devices page in the Apple Developer portal

See: https://linear.app/expo/issue/ENG-7591/detect-processing-devices-on-apple-developer-portal-and-dont-allow-the

* update CHANGELOG.md
2023-08-24 15:35:48 +02:00
Will Schurman 6cb02d0ccd [eas-cli][eas-update] Support republishing roll back to embedded updates (#2006) 2023-08-15 11:39:24 -04:00
Will Schurman d6c7623718 [eas-cli][eas-update] Add rollback disambiguation command (#2004) 2023-08-15 11:36:18 -04:00
Quinlan Jung 32142b4fe8 [eas-cli] more branch mapping utils (#2003)
* [eas-cli] more branch mapping utils

* update CHANGELOG.md
2023-08-14 14:51:29 -07:00
Will Schurman 6bdee0f395 [eas-cli][update] Only export at most ios and android dist (#2002) 2023-08-14 17:28:06 -04:00
Quinlan Jung baf94a21c3 [eas-cli] more branch map utility functions (#2001)
* [eas-cli] more branch map utility functions

* update CHANGELOG.md

* feedback
2023-08-11 11:35:47 -07:00
Radoslaw Krzemien 162d64bce6 Revert "[eas-cli] Update oclif version"
This reverts commit d16c1fc757.
2023-08-11 12:22:56 +02:00
Radoslaw Krzemien d16c1fc757 [eas-cli] Update oclif version
Updated oclif version

See: https://linear.app/expo/issue/ENG-9175/update-oclif-in-eas-cli
2023-08-11 12:22:17 +02:00
Expo CI e42ea7e2ec update CHANGELOG.md 2023-08-10 10:26:10 +00:00
Szymon Dziedzic 5fea1ad16e v4.1.2 2023-08-10 12:12:49 +02:00
Szymon Dziedzic 2a57a2eb3d upgrade eas-cli-local-build-plugin to 1.0.38 2023-08-10 12:12:41 +02:00
Quinlan Jung 5628b78523 [eas-cli] make branch mapping utility files have no dependencies (#1993)
* [eas-cli] generalize branch mapping utils

* make standalone

* update CHANGELOG.md
2023-08-09 15:32:19 -07:00
Expo CI 19d1e6bffd update CHANGELOG.md 2023-08-08 18:36:49 +00:00
Quinlan Jung 88b068511c v4.1.1 2023-08-08 11:28:16 -07:00
Quinlan Jung 842efcf3ab upgrade eas-cli-local-build-plugin to 1.0.35 2023-08-08 11:28:02 -07:00
Quinlan Jung f5f08aa506 [eas-cli] rollouts websupport (#1997)
* [eas-cli] rollouts websupport

* update CHANGELOG.md

* fix changelog
2023-08-08 11:26:18 -07:00
Expo CI 9f7f412cfb update CHANGELOG.md 2023-08-08 13:19:03 +00:00
Szymon Dziedzic e369ec1e25 v4.1.0 2023-08-08 15:10:39 +02:00
Szymon Dziedzic b353441f31 upgrade eas-cli-local-build-plugin to 1.0.34 2023-08-08 15:10:24 +02:00
Szymon Dziedzic 4db35e73c6 [eas-cli] bump @expo/steps version (#1995) 2023-08-08 15:02:33 +02:00
Szymon Dziedzic 10bc872dc9 [eas-json][eas-cli] add global withoutCredentials property to eas.json (#1994)
* [eas-json][eas-cli] add global `withoutCredentials` property to `eas.json`

* update CHANGELOG.md

* fix tests
2023-08-08 14:13:25 +02:00
Szymon Dziedzic fedc39ea48 [eas-cli] pass iOS credentials to custom builds (#1989)
* [eas-cli] pass iOS credentials to custom builds

* update CHANGELOG.md
2023-08-08 13:55:18 +02:00
Expo CI 6046723769 update CHANGELOG.md 2023-08-08 08:39:19 +00:00
Quinlan Jung afaee9df38 v4.0.1 2023-08-07 16:28:14 -07:00
Quinlan Jung de39d6f00d [eas-cli] account for empty branch mapping (#1992)
* [eas-cli] account for empty branch mapping

* update CHANGELOG.md
2023-08-07 16:24:32 -07:00
Expo CI 401751fffa update CHANGELOG.md 2023-08-07 20:40:01 +00:00
73 changed files with 3244 additions and 481 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Expo Developers Discord
url: https://chat.expo.dev/
about: Join other developers in discussions regarding Expo and EAS.
+92
View File
@@ -6,6 +6,98 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
- Rollouts: json output for ci. ([#2037](https://github.com/expo/eas-cli/pull/2037) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Update: only print channel-branch pairing if we created a channel. ([#2036](https://github.com/expo/eas-cli/pull/2036) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Added rollout tests. ([#2042](https://github.com/expo/eas-cli/pull/2042) by [@quinlanj](https://github.com/quinlanj))
- Remove unreachable codesigning option. ([#2041](https://github.com/expo/eas-cli/pull/2041) by [@quinlanj](https://github.com/quinlanj))
- Fix generated graphql tsc errors. ([#2039](https://github.com/expo/eas-cli/pull/2039) by [@quinlanj](https://github.com/quinlanj))
- Rollouts: view action for CI. ([#2040](https://github.com/expo/eas-cli/pull/2040) by [@quinlanj](https://github.com/quinlanj))
## [5.1.0](https://github.com/expo/eas-cli/releases/tag/v5.1.0) - 2023-09-01
### 🎉 New features
- Support `pnpm` option in eas.json. ([#1988](https://github.com/expo/eas-cli/pull/1988) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
- Make app config error not repeat indefinitely. ([#2020](https://github.com/expo/eas-cli/pull/2020) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
## [5.0.2](https://github.com/expo/eas-cli/releases/tag/v5.0.2) - 2023-08-29
### 🐛 Bug fixes
- Add proper expo cli `--platform` flag handling when exporting updates. ([#1939](https://github.com/expo/eas-cli/pull/1939) by [@byCedric](https://github.com/byCedric))
## [5.0.1](https://github.com/expo/eas-cli/releases/tag/v5.0.1) - 2023-08-28
### 🐛 Bug fixes
- Pass `platform` argument to expo-cli correctly when using the `eas update` command. ([#2028](https://github.com/expo/eas-cli/pull/2028) by [@szdziedzic](https://github.com/szdziedzic))
## [5.0.0](https://github.com/expo/eas-cli/releases/tag/v5.0.0) - 2023-08-28
### 🛠 Breaking changes
- Only export at most ios and android dist for EAS updates. ([#2002](https://github.com/expo/eas-cli/pull/2002) by [@wschurman](https://github.com/wschurman))
### 🎉 New features
- Add rollback disambiguation command. ([#2004](https://github.com/expo/eas-cli/pull/2004) by [@wschurman](https://github.com/wschurman))
- Detect devices that fail to be provisioned, list them to the user and show the explanation message with the link to the devices page to check actual status. ([#2011](https://github.com/expo/eas-cli/pull/2011) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Add info to EAS Update asset upload process about asset counts and limits. ([#2013](https://github.com/expo/eas-cli/pull/2013) by [@wschurman](https://github.com/wschurman))
- .nvmrc support for setting node version. ([#1954](https://github.com/expo/eas-cli/pull/1954) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
- Support republishing roll back to embedded updates. ([#2006](https://github.com/expo/eas-cli/pull/2006) by [@wschurman](https://github.com/wschurman))
- Configure updates as well when somebody tries to run a build with channel set. ([#2016](https://github.com/expo/eas-cli/pull/2016) by [@wschurman](https://github.com/wschurman))
- Fix printing bug: branch with no update. ([#2023](https://github.com/expo/eas-cli/pull/2023) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- More branch map utility functions. ([#2001](https://github.com/expo/eas-cli/pull/2001) by [@quinlanj](https://github.com/quinlanj))
- More branch mapping utils. ([#2003](https://github.com/expo/eas-cli/pull/2003) by [@quinlanj](https://github.com/quinlanj))
- Create a linked channel on build if not exists. ([#2017](https://github.com/expo/eas-cli/pull/2017) by [@quinlanj](https://github.com/quinlanj))
- Add `developmentClient` to metadata. ([#2015](https://github.com/expo/eas-cli/pull/2015) by [@szdziedzic](https://github.com/szdziedzic))
## [4.1.2](https://github.com/expo/eas-cli/releases/tag/v4.1.2) - 2023-08-10
### 🧹 Chores
- Make branch mapping utility files have no dependencies. ([#1993](https://github.com/expo/eas-cli/pull/1993) by [@quinlanj](https://github.com/quinlanj))
## [4.1.1](https://github.com/expo/eas-cli/releases/tag/v4.1.1) - 2023-08-08
### 🧹 Chores
- Logger to say website support is coming soon for rollouts. ([#1997](https://github.com/expo/eas-cli/pull/1997) by [@quinlanj](https://github.com/quinlanj))
## [4.1.0](https://github.com/expo/eas-cli/releases/tag/v4.1.0) - 2023-08-08
### 🎉 New features
- Pass credentials to custom iOS builds. ([#1989](https://github.com/expo/eas-cli/pull/1989) by [@szdziedzic](https://github.com/szdziedzic))
- Add the `withoutCredentials` option as a common build profile field in `eas.json`. ([#1994](https://github.com/expo/eas-cli/pull/1994) by [@szdziedzic](https://github.com/szdziedzic))
## [4.0.1](https://github.com/expo/eas-cli/releases/tag/v4.0.1) - 2023-08-08
### 🐛 Bug fixes
- Fix `eas channel:*` to work with empty branch mappings. ([#1992](https://github.com/expo/eas-cli/pull/1992) by [@quinlanj](https://github.com/quinlanj))
## [4.0.0](https://github.com/expo/eas-cli/releases/tag/v4.0.0) - 2023-08-07
### 🛠 Breaking changes
- Release redesigned `eas channel:rollout` into developer preview. The set of flag arguments are different, in addition to the workflow. ([#1986](https://github.com/expo/eas-cli/pull/1986) by [@quinlanj](https://github.com/quinlanj))
### 🎉 New features
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Instead of opening a GitHub issue for security vulnerabilities, send an email to our secure inbox: secure@expo.io.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "4.0.0",
"version": "5.2.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+57 -57
View File
@@ -133,7 +133,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -150,7 +150,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -167,7 +167,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -181,7 +181,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -231,7 +231,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -252,7 +252,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -272,7 +272,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -292,7 +292,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -315,7 +315,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -345,7 +345,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -362,7 +362,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -379,7 +379,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -414,7 +414,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/v4.0.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -452,7 +452,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -478,7 +478,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -502,7 +502,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/run.ts)_
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -551,7 +551,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -570,7 +570,7 @@ DESCRIPTION
update version of an app
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v4.0.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/v5.2.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -589,7 +589,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/v4.0.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/v5.2.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -606,7 +606,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -627,7 +627,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -649,7 +649,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -669,7 +669,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -677,7 +677,7 @@ Roll a new branch out on a channel incrementally.
```
USAGE
$ eas channel:rollout [CHANNEL] [--action create|edit|end] [--percent <value>] [--outcome
$ eas channel:rollout [CHANNEL] [--action create|edit|end|view] [--percent <value>] [--outcome
republish-and-revert|revert] [--branch <value>] [--runtime-version <value>] [--private-key-path <value>] [--json
--non-interactive]
@@ -685,7 +685,7 @@ ARGUMENTS
CHANNEL channel on which the rollout should be done
FLAGS
--action=(create|edit|end) Rollout action to perform
--action=(create|edit|end|view) Rollout action to perform
--branch=<value> Branch to roll out. Use with --action=create
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
@@ -701,7 +701,7 @@ DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -724,7 +724,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -745,7 +745,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -762,7 +762,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -776,7 +776,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/v4.0.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -796,7 +796,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -817,7 +817,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -838,7 +838,7 @@ DESCRIPTION
rename a registered device
```
_See code: [src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -852,7 +852,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -866,7 +866,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -954,7 +954,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -971,7 +971,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/v4.0.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -988,7 +988,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/v4.0.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -1002,7 +1002,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1016,7 +1016,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1038,7 +1038,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -1061,7 +1061,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/v4.0.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1079,7 +1079,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1093,7 +1093,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1113,7 +1113,7 @@ DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -1143,7 +1143,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1176,7 +1176,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1194,7 +1194,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1215,7 +1215,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1237,7 +1237,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1264,7 +1264,7 @@ DESCRIPTION
roll back to an existing update
```
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:view GROUPID`
@@ -1284,7 +1284,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1305,7 +1305,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1325,7 +1325,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1343,7 +1343,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1365,7 +1365,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1382,7 +1382,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v4.0.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v5.2.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+1295 -92
View File
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "4.0.0",
"version": "5.2.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -13,8 +13,8 @@
"@expo/config": "8.1.2",
"@expo/config-plugins": "7.2.4",
"@expo/config-types": "49.0.0",
"@expo/eas-build-job": "1.0.30",
"@expo/eas-json": "4.0.0",
"@expo/eas-build-job": "1.0.39",
"@expo/eas-json": "5.2.0",
"@expo/json-file": "8.2.37",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
@@ -27,7 +27,7 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.31",
"@expo/steps": "1.0.34",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.23.2",
"@oclif/plugin-autocomplete": "1.3.10",
@@ -73,7 +73,7 @@
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.3.8",
"semver": "7.5.2",
"slash": "3.0.0",
"tar": "6.1.13",
"terminal-link": "2.1.1",
@@ -66,6 +66,7 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
pnpm: buildProfile.pnpm,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
+1
View File
@@ -55,4 +55,5 @@ export interface BuildContext<T extends Platform> {
customBuildConfigMetadata?: CustomBuildConfigMetadata;
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
developmentClient: boolean;
}
@@ -60,6 +60,12 @@ export async function createBuildContextAsync<T extends Platform>({
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = account.id;
const runFromCI = getenv.boolish('CI', false);
const developmentClient =
buildProfile.developmentClient ??
(platform === Platform.ANDROID
? (buildProfile as BuildProfile<Platform.ANDROID>)?.gradleCommand === ':app:assembleDebug'
: (buildProfile as BuildProfile<Platform.IOS>)?.buildConfiguration === 'Debug') ??
false;
const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
@@ -120,6 +126,7 @@ export async function createBuildContextAsync<T extends Platform>({
message,
runFromCI,
customBuildConfigMetadata,
developmentClient,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -55,6 +55,5 @@ export async function ensureIosCredentialsForBuildResignAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
const isCustomBuild = buildCtx.buildProfile.config !== undefined;
return !buildCtx.buildProfile.simulator && !isCustomBuild;
return !buildCtx.buildProfile.simulator && !buildCtx.buildProfile.withoutCredentials;
}
@@ -58,6 +58,7 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
pnpm: buildProfile.pnpm,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.33';
const PLUGIN_PACKAGE_VERSION = '1.0.42';
export enum LocalBuildMode {
/**
+1
View File
@@ -63,6 +63,7 @@ export async function collectMetadataAsync<T extends Platform>(
runFromCI: ctx.runFromCI,
buildMode: ctx.buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
customWorkflowName: ctx.customBuildConfigMetadata?.workflowName,
developmentClient: ctx.developmentClient,
};
return sanitizeMetadata(metadata);
}
@@ -1,3 +1,4 @@
import { ExpoConfig } from '@expo/config-types';
import { Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
@@ -13,6 +14,7 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { Analytics } from '../analytics/AnalyticsManager';
import { createAndLinkChannelAsync, doesChannelExistAsync } from '../channel/queries';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -38,9 +40,9 @@ import {
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import {
installExpoUpdatesAsync,
isExpoUpdatesInstalledAsDevDependency,
isExpoUpdatesInstalledOrAvailable,
isUsingEASUpdate,
validateAppVersionRuntimePolicySupportAsync,
} from '../project/projectUtils';
import {
@@ -57,6 +59,7 @@ import {
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
@@ -117,6 +120,7 @@ export async function runBuildAndSubmitAsync(
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
projectDir,
});
Log.log(
`Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
@@ -183,6 +187,7 @@ export async function runBuildAndSubmitAsync(
platforms,
profileName: flags.submitProfile,
type: 'submit',
projectDir,
});
for (const startedBuild of startedBuilds) {
const submitProfile = nullthrows(
@@ -320,11 +325,26 @@ async function prepareAndStartBuildAsync({
);
if (buildProfile.profile.channel) {
await validateExpoUpdatesInstalledAsProjectDependencyAsync({
graphqlClient,
exp: buildCtx.exp,
projectId: buildCtx.projectId,
projectDir,
sdkVersion: buildCtx.exp.sdkVersion,
nonInteractive: flags.nonInteractive,
buildProfile,
});
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
appId: buildCtx.projectId,
channelName: buildProfile.profile.channel,
});
if (!doesChannelExist) {
await createAndLinkChannelAsync(graphqlClient, {
appId: buildCtx.projectId,
channelName: buildProfile.profile.channel,
});
}
}
}
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
@@ -447,11 +467,17 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
}
async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
exp,
graphqlClient,
projectId,
projectDir,
buildProfile,
nonInteractive,
sdkVersion,
}: {
graphqlClient: ExpoGraphqlClient;
exp: ExpoConfig;
projectId: string;
projectDir: string;
buildProfile: ProfileData<'build'>;
nonInteractive: boolean;
@@ -467,18 +493,24 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
);
} else if (nonInteractive) {
Log.warn(
`The build profile "${buildProfile.profileName}" has specified the channel "${buildProfile.profile.channel}", but the "expo-updates" package hasn't been installed. To use channels for your builds, install the "expo-updates" package by running "npx expo install expo-updates".`
`The build profile "${buildProfile.profileName}" has specified the channel "${buildProfile.profile.channel}", but the "expo-updates" package hasn't been installed. To use channels for your builds, install the "expo-updates" package by running "npx expo install expo-updates" followed by "eas update:configure".`
);
} else {
Log.warn(
`The build profile "${buildProfile.profileName}" specifies the channel "${buildProfile.profile.channel}", but the "expo-updates" package is missing. To use channels in your builds, install the "expo-updates" package.`
`The build profile "${buildProfile.profileName}" specifies the channel "${buildProfile.profile.channel}", but the "expo-updates" package is missing. To use channels in your builds, install the "expo-updates" package and run "eas update:configure".`
);
const installExpoUpdates = await confirmAsync({
message: `Would you like to install the "expo-updates" package?`,
message: `Would you like to install the "expo-updates" package and configure EAS Update now?`,
});
if (installExpoUpdates) {
await installExpoUpdatesAsync(projectDir, { silent: false });
Log.withTick('Installed expo-updates');
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp,
projectId,
projectDir,
platform: RequestedPlatform.All,
});
Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.');
}
}
}
@@ -0,0 +1,22 @@
import { BranchBasicInfo, ChannelBasicInfo, UpdateChannelInfoWithBranches } from '../utils';
export const testChannelBasicInfo: ChannelBasicInfo = {
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
name: 'production',
branchMapping:
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
};
export const testBasicBranchInfo1: BranchBasicInfo = {
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
name: 'wrong-channel',
};
export const testBasicBranchInfo2: BranchBasicInfo = {
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
name: 'production',
};
export const channelInfoWithBranches: UpdateChannelInfoWithBranches<BranchBasicInfo> = {
...testChannelBasicInfo,
updateBranches: [testBasicBranchInfo1, testBasicBranchInfo2],
};
@@ -1,21 +1,41 @@
import { rolloutBranchMapping, standardBranchMapping } from '../../rollout/__tests__/fixtures';
import {
emptyBranchMapping,
rolloutBranchMapping,
standardBranchMapping,
} from '../../rollout/__tests__/fixtures';
import {
BranchMappingValidationError,
assertVersion,
getAlwaysTrueBranchMapping,
getBranchMapping,
getEmptyBranchMapping,
getStandardBranchId,
isAlwaysTrueBranchMapping,
isEmptyBranchMapping,
} from '../branch-mapping';
import { testChannelObject } from './fixtures';
import { testChannelBasicInfo } from './branch-mapping-fixtures';
describe(assertVersion, () => {
it('throws if the branch mapping is not the correct version', () => {
expect(() => assertVersion(testChannelObject, 5)).toThrowError(BranchMappingValidationError);
expect(() => assertVersion(testChannelBasicInfo, 5)).toThrowError(BranchMappingValidationError);
});
it('asserts the correct version', () => {
assertVersion(testChannelObject, 0);
expect(getBranchMapping(testChannelObject.branchMapping).version).toBe(0);
assertVersion(testChannelBasicInfo, 0);
expect(getBranchMapping(testChannelBasicInfo.branchMapping).version).toBe(0);
});
});
describe(getEmptyBranchMapping, () => {
it('gets an empty branch mapping', () => {
expect(isEmptyBranchMapping(getEmptyBranchMapping())).toBe(true);
});
});
describe(isEmptyBranchMapping, () => {
it('detects empty branch mappings', () => {
expect(isEmptyBranchMapping(emptyBranchMapping)).toBe(true);
expect(isEmptyBranchMapping(standardBranchMapping)).toBe(false);
expect(isEmptyBranchMapping(rolloutBranchMapping)).toBe(false);
});
});
@@ -23,6 +43,7 @@ describe(isAlwaysTrueBranchMapping, () => {
it('detects always true branch mappings', () => {
expect(isAlwaysTrueBranchMapping(standardBranchMapping)).toBe(true);
expect(isAlwaysTrueBranchMapping(rolloutBranchMapping)).toBe(false);
expect(isAlwaysTrueBranchMapping(emptyBranchMapping)).toBe(false);
});
});
@@ -35,10 +56,12 @@ describe(getAlwaysTrueBranchMapping, () => {
describe(getStandardBranchId, () => {
it('throws if the branch mapping is not a standard mapping', () => {
expect(() => getStandardBranchId(testChannelObject)).toThrowError(BranchMappingValidationError);
expect(() => getStandardBranchId(testChannelBasicInfo)).toThrowError(
BranchMappingValidationError
);
});
it('gets a standard branch id', () => {
const channelObjectWithStandardMapping = { ...testChannelObject };
const channelObjectWithStandardMapping = { ...testChannelBasicInfo };
channelObjectWithStandardMapping.branchMapping = JSON.stringify(standardBranchMapping);
expect(getStandardBranchId(channelObjectWithStandardMapping)).toBe(
standardBranchMapping.data[0].branchId
@@ -124,6 +124,7 @@ export const testChannelObject: UpdateChannelObject = {
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
name: 'production',
createdAt: '2023-05-16T21:26:16.946Z',
updatedAt: '2023-05-16T21:26:16.946Z',
branchMapping:
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
updateBranches: [testUpdateBranch1, testUpdateBranch2],
@@ -0,0 +1,54 @@
import { getAlwaysTrueBranchMapping, getEmptyBranchMapping } from '../branch-mapping';
import { getDescriptionByBranchId } from '../print-utils';
import { testChannelObject, testUpdateBranch1, testUpdateBranch2 } from './fixtures';
describe(getDescriptionByBranchId, () => {
it('should get descriptions for multiple branches with multiple update groups', () => {
const descriptionByBranchId = getDescriptionByBranchId(testChannelObject);
expect(Object.values(descriptionByBranchId)).toHaveLength(2);
expect(descriptionByBranchId[testUpdateBranch1.id]).toEqual({
branch: 'wrong-channel',
branchRolloutPercentage: 15,
update: {
codeSigningKey: undefined,
group: '16ca6dba-e63b-48b0-baa3-15a894ee9434',
isRollBackToEmbedded: false,
message: '"fix bug" (1 month ago by quintest113)',
platforms: 'android, ios',
runtimeVersion: 'exposdk:48.0.0',
},
});
expect(descriptionByBranchId[testUpdateBranch2.id]).toEqual({
branch: 'production',
branchRolloutPercentage: 85,
update: {
codeSigningKey: undefined,
group: 'e40ad156-e9af-4cc2-8e9d-c7b5c328db48',
isRollBackToEmbedded: false,
message: '"fix bug" (2 months ago by quintest113)',
platforms: 'android, ios',
runtimeVersion: 'exposdk:48.0.0',
},
});
});
it('should get descriptions for branches with no updates', () => {
const noUpdatesBranch = { ...testUpdateBranch1, updateGroups: [] };
const oneBranchChannel = {
...testChannelObject,
updateBranches: [noUpdatesBranch],
branchMapping: JSON.stringify(getAlwaysTrueBranchMapping(noUpdatesBranch.id)),
};
const descriptionByBranchId = getDescriptionByBranchId(oneBranchChannel);
expect(Object.values(descriptionByBranchId)).toHaveLength(1);
expect(descriptionByBranchId[testUpdateBranch1.id]).toEqual({ branch: 'wrong-channel' });
});
it('should get descriptions for no branches', () => {
const noBranchChannel = {
...testChannelObject,
updateBranches: [],
branchMapping: JSON.stringify(getEmptyBranchMapping()),
};
const descriptionByBranchId = getDescriptionByBranchId(noBranchChannel);
expect(Object.values(descriptionByBranchId)).toHaveLength(0);
});
});
@@ -1,11 +1,11 @@
import { getUpdateBranch } from '../utils';
import { testChannelObject, testUpdateBranch1 } from './fixtures';
import { channelInfoWithBranches, testBasicBranchInfo1 } from './branch-mapping-fixtures';
describe(getUpdateBranch, () => {
it('gets the update branch', () => {
expect(getUpdateBranch(testChannelObject, '754bf17f-efc0-46ab-8a59-a03f20e53e9b')).toBe(
testUpdateBranch1
expect(getUpdateBranch(channelInfoWithBranches, '754bf17f-efc0-46ab-8a59-a03f20e53e9b')).toBe(
testBasicBranchInfo1
);
expect(() => getUpdateBranch(testChannelObject, 'not-a-branch-ID')).toThrow();
expect(() => getUpdateBranch(channelInfoWithBranches, 'not-a-branch-ID')).toThrow();
});
});
+27 -4
View File
@@ -1,4 +1,4 @@
import { UpdateChannelBasicInfoFragment } from '../graphql/generated';
import { ChannelBasicInfo } from './utils';
// TODO(quin): move this into a common package with www
export type BranchMappingOperator =
@@ -45,6 +45,18 @@ export type AlwaysTrueBranchMapping = {
];
};
export type EmptyBranchMapping = {
version: number;
data: [];
};
export function getEmptyBranchMapping(): EmptyBranchMapping {
return {
version: 0,
data: [],
};
}
export function getAlwaysTrueBranchMapping(branchId: string): AlwaysTrueBranchMapping {
return {
version: 0,
@@ -57,17 +69,28 @@ export function getAlwaysTrueBranchMapping(branchId: string): AlwaysTrueBranchMa
};
}
export function hasStandardBranchMap(channelInfo: UpdateChannelBasicInfoFragment): boolean {
export function hasEmptyBranchMap(channelInfo: ChannelBasicInfo): boolean {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
return isEmptyBranchMapping(branchMapping);
}
export function hasStandardBranchMap(channelInfo: ChannelBasicInfo): boolean {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
return isAlwaysTrueBranchMapping(branchMapping);
}
export function getStandardBranchId(channelInfo: UpdateChannelBasicInfoFragment): string {
export function getStandardBranchId(channelInfo: ChannelBasicInfo): string {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
assertAlwaysTrueBranchMapping(branchMapping);
return getBranchIdFromStandardMapping(branchMapping);
}
export function isEmptyBranchMapping(
branchMapping: BranchMapping
): branchMapping is EmptyBranchMapping {
return branchMapping.data.length === 0;
}
export function isAlwaysTrueBranchMapping(
branchMapping: BranchMapping
): branchMapping is AlwaysTrueBranchMapping {
@@ -137,7 +160,7 @@ function isVersion(branchMapping: BranchMapping, version: number): boolean {
return branchMapping.version === version;
}
export function assertVersion(channelInfo: UpdateChannelBasicInfoFragment, version: number): void {
export function assertVersion(channelInfo: ChannelBasicInfo, version: number): void {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
if (!isVersion(branchMapping, version)) {
throw new BranchMappingValidationError(
+42 -26
View File
@@ -1,4 +1,3 @@
import assert from 'assert';
import chalk from 'chalk';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
@@ -9,19 +8,37 @@ import {
formatBranch,
getUpdateGroupDescriptionsWithBranch,
} from '../update/utils';
import { assertVersion, hasStandardBranchMap } from './branch-mapping';
import { assertVersion, hasEmptyBranchMap, hasStandardBranchMap } from './branch-mapping';
/**
* Log all the branches associated with the channel and their most recent update group
*/
export function logChannelDetails(channel: UpdateChannelObject): void {
assertVersion(channel, 0);
assert(
hasStandardBranchMap(channel) || isRollout(channel),
'Only standard branch mappings and rollouts are supported.'
);
const doesSupportMapping =
hasEmptyBranchMap(channel) || hasStandardBranchMap(channel) || isRollout(channel);
if (!doesSupportMapping) {
Log.log(chalk.dim('Custom branch mapping detected.'));
return;
}
const branchDescription = channel.updateBranches.flatMap(branch => {
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
branch.updateGroups
const branchDescriptionByBranchId = getDescriptionByBranchId(channel);
const entries = Object.entries(branchDescriptionByBranchId);
if (entries.length === 0) {
Log.log(chalk.dim('No branches are pointed to this channel.'));
} else {
Log.log(
entries
.map(([_branchId, description]) => formatBranch(description))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}
export function getDescriptionByBranchId(
channel: UpdateChannelObject
): Record<string, FormattedBranchDescription> {
return channel.updateBranches.reduce((acc, branch) => {
const maybeRollout = isRollout(channel) ? getRollout(channel) : null;
let maybePercentOnBranch: number | undefined = undefined;
if (maybeRollout) {
@@ -31,22 +48,21 @@ export function logChannelDetails(channel: UpdateChannelObject): void {
: 100 - maybeRollout.percentRolledOut;
}
return updateGroupWithBranchDescriptions.map(
({ branch, ...updateGroup }): FormattedBranchDescription => ({
branch,
branchRolloutPercentage: maybePercentOnBranch,
update: updateGroup,
})
if (branch.updateGroups.length === 0) {
acc[branch.id] = { branch: branch.name, branchRolloutPercentage: maybePercentOnBranch };
return acc;
}
const updateGroupsWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
branch.updateGroups
);
});
if (branchDescription.length === 0) {
Log.log(chalk.dim('No branches are pointed to this channel.'));
} else {
Log.log(
branchDescription
.map(description => formatBranch(description))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
// display the most recent update group
const updateGroupWithBranchDescription = updateGroupsWithBranchDescriptions[0];
const { branch: branchName, ...updateGroup } = updateGroupWithBranchDescription;
acc[branch.id] = {
branch: branchName,
branchRolloutPercentage: maybePercentOnBranch,
update: updateGroup,
};
return acc;
}, {} as Record<string, FormattedBranchDescription>);
}
+103
View File
@@ -2,6 +2,8 @@ import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { createUpdateBranchOnAppAsync } from '../branch/queries';
import { BranchNotFoundError } from '../branch/utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
@@ -15,13 +17,16 @@ import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/Bra
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
import Log from '../log';
import { getDisplayNameForProjectIdAsync } from '../project/projectUtils';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { ChannelNotFoundError } from './errors';
import { logChannelDetails } from './print-utils';
import { ChannelBasicInfo } from './utils';
export const CHANNELS_LIMIT = 25;
@@ -264,3 +269,101 @@ export async function ensureChannelExistsAsync(
}
}
}
export async function doesChannelExistAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, channelName }: { appId: string; channelName: string }
): Promise<boolean> {
try {
await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId,
channelName,
});
} catch (err) {
if (err instanceof ChannelNotFoundError) {
return false;
}
throw err;
}
return true;
}
/**
*
* Creates a channel and links it to a branch with the same name.
*
* @param appId the app ID, also known as the project ID
* @param channelName the name of the channel to create
* @param shouldPrintJson print only the JSON output
*/
export async function createAndLinkChannelAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
channelName,
shouldPrintJson,
}: { appId: string; channelName: string; shouldPrintJson?: boolean }
): Promise<ChannelBasicInfo> {
let branchId: string;
let branchMessage: string;
try {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: channelName,
});
branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
} else {
throw error;
}
}
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
appId,
channelName,
branchId,
});
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on project with id ${appId}`
);
}
if (shouldPrintJson) {
printJsonOnlyOutput(newChannel);
} else {
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(
await getDisplayNameForProjectIdAsync(graphqlClient, appId)
)}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
}
return newChannel;
}
+19 -7
View File
@@ -1,18 +1,30 @@
import { UpdateBranchObject, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
export type ChannelBasicInfo = {
id: string;
name: string;
branchMapping: string;
};
export type BranchBasicInfo = {
id: string;
name: string;
};
function getUpdateBranchNullable(
channel: UpdateChannelObject,
export type UpdateChannelInfoWithBranches<Branch extends BranchBasicInfo> = ChannelBasicInfo & {
updateBranches: Branch[];
};
function getUpdateBranchNullable<Branch extends BranchBasicInfo>(
channel: UpdateChannelInfoWithBranches<Branch>,
branchId: string
): UpdateBranchObject | null {
): Branch | null {
const updateBranches = channel.updateBranches;
const updateBranch = updateBranches.find(branch => branch.id === branchId);
return updateBranch ?? null;
}
export function getUpdateBranch(
channel: UpdateChannelObject,
export function getUpdateBranch<Branch extends BranchBasicInfo>(
channel: UpdateChannelInfoWithBranches<Branch>,
branchId: string
): UpdateBranchObject {
): Branch {
const updateBranch = getUpdateBranchNullable(channel, branchId);
if (!updateBranch) {
throw new Error(
@@ -68,6 +68,7 @@ export default class BuildVersionGetView extends EasCommand {
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
projectDir,
});
const results: { [key in Platform]?: string } = {};
for (const { profile, platform } of buildProfiles) {
@@ -80,6 +80,7 @@ export default class BuildVersionSyncView extends EasCommand {
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
projectDir,
});
for (const profileInfo of buildProfiles) {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
@@ -1,16 +1,11 @@
import chalk from 'chalk';
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError } from '../../branch/utils';
import { createChannelOnAppAsync } from '../../channel/queries';
import { createAndLinkChannelAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { enableJsonOutput } from '../../utils/json';
export default class ChannelCreate extends EasCommand {
static override description = 'create a channel';
@@ -60,69 +55,13 @@ export default class ChannelCreate extends EasCommand {
}));
}
let branchId: string;
let branchMessage: string;
try {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId: projectId,
name: channelName,
});
branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId: projectId,
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
} else {
throw error;
}
}
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
await createAndLinkChannelAsync(graphqlClient, {
appId: projectId,
channelName,
branchId,
shouldPrintJson: jsonFlag,
});
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on project with id ${projectId}`
);
}
if (jsonFlag) {
printJsonOnlyOutput(newChannel);
} else {
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(
await getDisplayNameForProjectIdAsync(graphqlClient, projectId)
)}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
}
@@ -4,7 +4,7 @@ import { print } from 'graphql';
import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { hasStandardBranchMap } from '../../channel/branch-mapping';
import { hasEmptyBranchMap, hasStandardBranchMap } from '../../channel/branch-mapping';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
@@ -102,7 +102,7 @@ export default class ChannelEdit extends EasCommand {
if (isRollout(existingChannel)) {
throw new Error('There is a rollout in progress. Manage it with "channel:rollout" instead.');
} else if (!hasStandardBranchMap(existingChannel)) {
} else if (!hasStandardBranchMap(existingChannel) && !hasEmptyBranchMap(existingChannel)) {
throw new Error('Only standard branch mappings can be edited with this command.');
}
@@ -17,11 +17,13 @@ import {
RolloutActions,
RolloutMainMenu,
} from '../../rollout/actions/RolloutMainMenu';
import { enableJsonOutput } from '../../utils/json';
enum ActionRawFlagValue {
CREATE = 'create',
EDIT = 'edit',
END = 'end',
VIEW = 'view',
}
type ChannelRolloutRawArgsAndFlags = {
channel?: string;
@@ -137,8 +139,7 @@ export default class ChannelRollout extends EasCommand {
nonInteractive: argsAndFlags.nonInteractive,
});
if (argsAndFlags.json) {
// TODO(quin): implement json output
throw new Error('Developer Preview doesnt support JSON output yet');
enableJsonOutput();
}
const app = { projectId, exp };
@@ -153,7 +154,7 @@ export default class ChannelRollout extends EasCommand {
} else {
Log.addNewLineIfNone();
Log.warn(
`✨ This command is in Developer Preview and has not been released to production yet`
`✨ This command is in Developer Preview and has not been released to production yet. Website support is coming soon.`
);
Log.addNewLineIfNone();
await new RolloutMainMenu(argsAndFlags).runAsync(ctx);
@@ -168,6 +169,8 @@ export default class ChannelRollout extends EasCommand {
return ManageRolloutActions.EDIT;
case ActionRawFlagValue.END:
return ManageRolloutActions.END;
case ActionRawFlagValue.VIEW:
return ManageRolloutActions.VIEW;
}
}
@@ -47,6 +47,7 @@ export default class MetadataLint extends EasCommand {
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
projectDir,
});
if (submitProfiles.length !== 1) {
@@ -49,6 +49,7 @@ export default class MetadataPull extends EasCommand {
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
projectDir,
});
if (submitProfiles.length !== 1) {
@@ -47,6 +47,7 @@ export default class MetadataPush extends EasCommand {
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
projectDir,
});
if (submitProfiles.length !== 1) {
+1
View File
@@ -118,6 +118,7 @@ export default class Submit extends EasCommand {
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms,
profileName: flagsWithPlatform.profile,
projectDir,
});
const submissions: SubmissionFragment[] = [];
@@ -57,12 +57,6 @@ jest.mock('../../../fetch');
describe(UpdateRepublish.name, () => {
afterEach(() => vol.reset());
it('errors without --channel, --branch, or --group', async () => {
await expect(new UpdateRepublish([], commandOptions).run()).rejects.toThrow(
'--channel, --branch, or --group must be specified'
);
});
it('errors when providing both --group and --branch', async () => {
const flags = ['--group=1234', '--branch=main'];
+19 -2
View File
@@ -35,6 +35,7 @@ import {
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
platformDisplayNames,
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
@@ -325,6 +326,21 @@ export default class UpdatePublish extends EasCommand {
for (const uploadedAssetPath of uploadResults.uniqueUploadedAssetPaths) {
Log.debug(chalk.dim(`- ${uploadedAssetPath}`));
}
const platformString = (Object.keys(assets) as PublishPlatform[])
.map(platform => {
const collectedAssetForPlatform = nullthrows(assets[platform]);
const totalAssetsForPlatform = collectedAssetForPlatform.assets.length + 1; // launch asset
const assetString = totalAssetsForPlatform === 1 ? 'asset' : 'assets';
return `${totalAssetsForPlatform} ${platformDisplayNames[platform]} ${assetString}`;
})
.join(', ');
Log.withInfo(
`${platformString} (maximum: ${assetLimitPerUpdateGroup} total per update). ${learnMore(
'https://expo.fyi/eas-update-asset-limits',
{ learnMoreMessage: 'Learn more about asset limits.' }
)}`
);
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
@@ -344,10 +360,11 @@ export default class UpdatePublish extends EasCommand {
branchId,
channelName: branchName,
});
Log.withTick(
`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`
);
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const vcsClient = getVcsClient();
const gitCommitHash = await vcsClient.getCommitHashAsync();
@@ -1,6 +1,8 @@
import { Platform } from '@expo/config';
import { Flags } from '@oclif/core';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
@@ -151,9 +153,6 @@ export default class UpdateRepublish extends EasCommand {
if (nonInteractive && !groupId) {
throw new Error('Only --group can be used in non-interactive mode');
}
if (!groupId && !(branchName || channelName)) {
throw new Error(`--channel, --branch, or --group must be specified`);
}
const platform =
rawFlags.platform === 'all' ? defaultRepublishPlatforms : ([rawFlags.platform] as Platform[]);
@@ -190,6 +189,10 @@ async function getOrAskUpdatesAsync(
}));
}
if (flags.nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
if (flags.branchName) {
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
@@ -206,7 +209,55 @@ async function getOrAskUpdatesAsync(
});
}
throw new Error('--channel, --branch, or --group is required');
const { choice } = await promptAsync({
type: 'select',
message: 'Find update by branch or channel?',
name: 'choice',
choices: [
{ title: 'Branch', value: 'branch' },
{ title: 'Channel', value: 'channel' },
],
});
if (choice === 'channel') {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel to view',
paginatedQueryOptions: {
json: flags.json,
nonInteractive: flags.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromChannelNameAsync(graphqlClient, {
...flags,
channelName: name,
projectId,
});
} else if (choice === 'branch') {
const { name } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Select branch from which to choose update',
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: flags.json,
nonInteractive: flags.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
branchName: name,
projectId,
});
} else {
throw new Error('Must choose update via channel or branch');
}
}
/** Ask the user which update needs to be republished by branch name, this requires interactive mode */
@@ -219,10 +270,6 @@ async function askUpdatesFromBranchNameAsync(
nonInteractive,
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
const updateGroup = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
@@ -246,10 +293,6 @@ async function askUpdatesFromChannelNameAsync(
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
const branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
return await askUpdatesFromBranchNameAsync(graphqlClient, {
@@ -86,7 +86,11 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
}),
platform: Flags.enum({
char: 'p',
options: [...defaultPublishPlatforms, 'all'],
options: [
// TODO: Add web when it's fully supported
...defaultPublishPlatforms,
'all',
],
default: 'all',
required: false,
}),
@@ -0,0 +1,42 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { promptAsync } from '../../prompts';
import UpdateRepublish from './republish';
import UpdateRollBackToEmbedded from './roll-back-to-embedded';
export default class UpdateRollback extends EasCommand {
static override description = 'roll back to an embedded update or an existing update';
static override hidden = true;
static override flags = {
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
}),
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(UpdateRollback);
const { choice } = await promptAsync({
type: 'select',
message: 'Which type of update would you like to roll back to?',
name: 'choice',
choices: [
{ title: 'Published Update', value: 'published' },
{ title: 'Embedded Update', value: 'embedded' },
],
});
const privateKeyPathArg = flags['private-key-path']
? ['--private-key-path', flags['private-key-path']]
: [];
if (choice === 'published') {
await UpdateRepublish.run(privateKeyPathArg);
} else {
await UpdateRollBackToEmbedded.run(privateKeyPathArg);
}
}
}
@@ -11,6 +11,7 @@ import {
} from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AndroidPackageNotDefinedError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
/**
@@ -111,7 +112,7 @@ export async function getAppLookupParamsFromContextAsync(
gradleContext
);
if (!androidApplicationIdentifier) {
throw new Error(
throw new AndroidPackageNotDefinedError(
`android.package needs to be defined in your ${getProjectConfigDescription(
ctx.projectDir
)} file`
@@ -15,3 +15,9 @@ export class UnsupportedCredentialsChoiceError extends Error {
super(message);
}
}
export class AndroidPackageNotDefinedError extends Error {
constructor(message?: string) {
super(message ?? 'android.package needs to be defined in your app.json/app.config.js file');
}
}
@@ -177,7 +177,23 @@ export class SetUpAdhocProvisioningProfile {
);
}
// 6. Create (or update) app build credentials
// 6. Compare selected devices with the ones actually provisioned
const diffList = differenceBy(
chosenDevices,
appleProvisioningProfile.appleDevices,
'identifier'
);
if (diffList && diffList.length > 0) {
Log.warn(`Failed to provision ${diffList.length} of the selected devices:`);
for (const missingDevice of diffList) {
Log.warn(`- ${formatDeviceLabel(missingDevice)}`);
}
Log.log(
'Most commonly devices fail to to be provisioned while they are still being processed by Apple, which can take up to 24-72 hours. Check your Apple Developer Portal page at https://developer.apple.com/account/resources/devices/list, the devices in "Processing" status cannot be provisioned yet'
);
}
// 7. Create (or update) app build credentials
assert(appleProvisioningProfile);
return await assignBuildCredentialsAsync(
ctx,
@@ -1,4 +1,38 @@
import { doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
import { Env } from '@expo/eas-build-job';
import { EasJson } from '@expo/eas-json';
import { Analytics } from '../../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
Account,
AppleAppIdentifierFragment,
AppleDevice,
AppleDeviceClass,
AppleDeviceFragment,
AppleDistributionCertificateFragment,
IosAppBuildCredentialsFragment,
} from '../../../../graphql/generated';
import Log from '../../../../log';
import { getApplePlatformFromTarget } from '../../../../project/ios/target';
import { Actor } from '../../../../user/User';
import { CredentialsContext, CredentialsContextProjectInfo } from '../../../context';
import { ProvisioningProfile } from '../../appstore/Credentials.types';
import { ApplePlatform } from '../../appstore/constants';
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from '../BuildCredentialsUtils';
import { chooseDevicesAsync } from '../DeviceUtils';
import { SetUpAdhocProvisioningProfile, doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
jest.mock('../BuildCredentialsUtils');
jest.mock('../../../context');
jest.mock('../../../ios/api/GraphqlClient');
jest.mock('../DeviceUtils', () => {
return {
__esModule: true,
chooseDevicesAsync: jest.fn(),
formatDeviceLabel: jest.requireActual('../DeviceUtils').formatDeviceLabel,
};
});
jest.mock('../../../../project/ios/target');
describe(doUDIDsMatch, () => {
it('return false if UDIDs do not match', () => {
@@ -12,3 +46,104 @@ describe(doUDIDsMatch, () => {
expect(doUDIDsMatch(udidsA, udidsB)).toBe(true);
});
});
describe('runWithDistributionCertificateAsync', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const setUpAdhocProvisioningProfile = new SetUpAdhocProvisioningProfile({
app: { account: {} as Account, projectName: 'projName', bundleIdentifier: 'bundleId' },
target: { targetName: 'targetName', bundleIdentifier: 'bundleId', entitlements: {} },
});
describe('compare chosen and provisioned devices', () => {
describe('not all devices provisioned', () => {
it('displays warning to the user and lists the missing devices', async () => {
const { ctx, distCert } = setUpTest();
jest.mocked(getBuildCredentialsAsync).mockResolvedValue({
provisioningProfile: {
appleTeam: {},
appleDevices: [{ identifier: 'id1' }],
developerPortalIdentifier: 'provisioningProfileId',
},
} as IosAppBuildCredentialsFragment);
const LogWarnSpy = jest.spyOn(Log, 'warn');
const LogLogSpy = jest.spyOn(Log, 'log');
const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(
ctx,
distCert
);
expect(result).toEqual({} as IosAppBuildCredentialsFragment);
expect(LogWarnSpy).toHaveBeenCalledTimes(3);
expect(LogWarnSpy).toHaveBeenCalledWith('Failed to provision 2 of the selected devices:');
expect(LogWarnSpy).toHaveBeenCalledWith('- id2 (iPhone) (Device 2)');
expect(LogWarnSpy).toHaveBeenCalledWith('- id3 (Mac) (Device 3)');
expect(LogLogSpy).toHaveBeenCalledTimes(1);
expect(LogLogSpy).toHaveBeenCalledWith(
'Most commonly devices fail to to be provisioned while they are still being processed by Apple, which can take up to 24-72 hours. Check your Apple Developer Portal page at https://developer.apple.com/account/resources/devices/list, the devices in "Processing" status cannot be provisioned yet'
);
});
});
describe('all devices provisioned', () => {
it('does not display warning', async () => {
const { ctx, distCert } = setUpTest();
jest.mocked(getBuildCredentialsAsync).mockResolvedValue({
provisioningProfile: {
appleTeam: {},
appleDevices: [{ identifier: 'id1' }, { identifier: 'id2' }, { identifier: 'id3' }],
developerPortalIdentifier: 'provisioningProfileId',
},
} as IosAppBuildCredentialsFragment);
const LogWarnSpy = jest.spyOn(Log, 'warn');
const LogLogSpy = jest.spyOn(Log, 'log');
const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(
ctx,
distCert
);
expect(result).toEqual({} as IosAppBuildCredentialsFragment);
expect(LogWarnSpy).not.toHaveBeenCalled();
expect(LogLogSpy).not.toHaveBeenCalled();
});
});
});
});
function setUpTest(): { ctx: CredentialsContext; distCert: AppleDistributionCertificateFragment } {
const ctx = jest.mocked(
new CredentialsContext(
{} as {
projectInfo: CredentialsContextProjectInfo | null;
easJsonCliConfig?: EasJson['cli'];
nonInteractive: boolean;
projectDir: string;
user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
env?: Env;
}
)
);
Object.defineProperty(ctx, 'ios', { value: jest.mock('../../../ios/api/GraphqlClient') });
ctx.ios.getDevicesForAppleTeamAsync = jest
.fn()
.mockResolvedValue([
{ identifier: 'id1' },
{ identifier: 'id2' },
{ identifier: 'id3' },
] as AppleDeviceFragment[]);
jest.mocked(chooseDevicesAsync).mockResolvedValue([
{ identifier: 'id1', name: 'Device 1', deviceClass: AppleDeviceClass.Ipad },
{ identifier: 'id2', name: 'Device 2', deviceClass: AppleDeviceClass.Iphone },
{ identifier: 'id3', name: 'Device 3', deviceClass: AppleDeviceClass.Mac },
] as AppleDevice[]);
// @ts-ignore
jest.mocked(getApplePlatformFromTarget).mockResolvedValue(ApplePlatform.IOS);
Object.defineProperty(ctx, 'appStore', { value: jest.mock('../../appstore/AppStoreApi') });
ctx.appStore.createOrReuseAdhocProvisioningProfileAsync = jest.fn().mockResolvedValue({
provisioningProfileId: 'provisioningProfileId',
} as ProvisioningProfile);
ctx.ios.createOrGetExistingAppleAppIdentifierAsync = jest
.fn()
.mockResolvedValue({} as AppleAppIdentifierFragment);
jest.mocked(assignBuildCredentialsAsync).mockResolvedValue({} as IosAppBuildCredentialsFragment);
const distCert = {} as AppleDistributionCertificateFragment;
return { ctx, distCert };
}
@@ -27,6 +27,7 @@ import {
displayEmptyAndroidCredentials,
} from '../android/utils/printCredentials';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
import { AndroidPackageNotDefinedError } from '../errors';
import { ActionInfo, AndroidActionType, Scope } from './Actions';
import {
buildCredentialsActions,
@@ -131,6 +132,12 @@ export class ManageAndroid {
await this.runProjectSpecificActionAsync(ctx, chosenAction, gradleContext);
} catch (err) {
if (err instanceof AndroidPackageNotDefinedError) {
err.message += `\n${learnMore(
'https://docs.expo.dev/workflow/configuration/'
)} about configuration with app.json/app.config.js`;
throw err; // breaks out of the loop to avoid failing again after continuation
}
Log.error(err);
if (process.env.DEBUG) {
throw err; // breaks out of the loop so we can get stack trace
@@ -0,0 +1,69 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonUtils } from '@expo/eas-json';
import { Analytics } from '../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { learnMore } from '../../../log';
import { getProjectConfigDescription } from '../../../project/projectUtils';
import { pressAnyKeyToContinueAsync } from '../../../prompts';
import { Actor } from '../../../user/User';
import { getAppLookupParamsFromContextAsync } from '../../android/actions/BuildCredentialsUtils';
import { CredentialsContextProjectInfo } from '../../context';
import { AndroidPackageNotDefinedError } from '../../errors';
import { Action } from '../HelperActions';
import { ManageAndroid } from '../ManageAndroid';
jest.mock('../../android/actions/BuildCredentialsUtils');
jest.mock('../../../prompts', () => {
return {
...jest.requireActual('../../../prompts'),
pressAnyKeyToContinueAsync: jest.fn(),
};
});
jest.mock('../../../project/projectUtils');
describe('runAsync', () => {
describe('"android.package" missing in app.json', () => {
const manageAndroid = new ManageAndroid(
{
projectInfo: {} as CredentialsContextProjectInfo,
actor: {} as Actor,
graphqlClient: {} as ExpoGraphqlClient,
analytics: {} as Analytics,
getDynamicPrivateProjectConfigAsync: jest
.fn()
.mockResolvedValue({ exp: {}, projectId: '' }),
runAsync: jest.fn(),
} as Action,
''
);
it('does not repeat the error indefinitely and prints useful error', async () => {
jest.spyOn(EasJsonUtils, 'getBuildProfileNamesAsync').mockResolvedValue(['testProfile']);
jest
.spyOn(EasJsonUtils, 'getBuildProfileAsync')
.mockResolvedValue({} as BuildProfile<Platform.ANDROID>);
jest.mocked(getProjectConfigDescription).mockReturnValue('app.json');
jest.mocked(getAppLookupParamsFromContextAsync).mockImplementation(() => {
throw new AndroidPackageNotDefinedError(
'Specify "android.package" in app.json and run this command again.'
);
});
const pressAnyKeyToContinueAsyncMock = jest.mocked(pressAnyKeyToContinueAsync);
Array.from(Array(100)).map((_, i) => {
// continue 101 times if error is rethrown indefinitely
pressAnyKeyToContinueAsyncMock.mockResolvedValueOnce();
});
pressAnyKeyToContinueAsyncMock.mockImplementationOnce(async () => {
// fail after 102nd time
fail('test should not reach this place - if it does, the error repeats indefinitely');
});
const reThrownError = new AndroidPackageNotDefinedError(
'Specify "android.package" in app.json and run this command again.\n' +
`${learnMore(
'https://docs.expo.dev/workflow/configuration/'
)} about configuration with app.json/app.config.js`
);
await expect(manageAndroid.runAsync()).rejects.toThrow(reThrownError);
});
});
});
+209 -18
View File
@@ -479,17 +479,11 @@ export type AccountSsoConfiguration = {
};
export type AccountSsoConfigurationData = {
authEndpoint?: InputMaybe<Scalars['String']>;
authProtocol: AuthProtocolType;
authProviderIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
clientSecret: Scalars['String'];
endSessionEndpoint?: InputMaybe<Scalars['String']>;
issuer: Scalars['String'];
jwksEndpoint?: InputMaybe<Scalars['String']>;
revokeEndpoint?: InputMaybe<Scalars['String']>;
tokenEndpoint?: InputMaybe<Scalars['String']>;
userInfoEndpoint?: InputMaybe<Scalars['String']>;
};
export type AccountSsoConfigurationMutation = {
@@ -602,6 +596,8 @@ export type Actor = {
* For example, when displaying a sentence indicating that actor X created a build or published an update.
*/
displayName: Scalars['String'];
/** Experiments associated with this actor */
experiments: Array<ActorExperiment>;
/**
* Server feature gate values for this actor, optionally filtering by desired gates.
* Only resolves for the viewer.
@@ -618,9 +614,33 @@ export type ActorFeatureGatesArgs = {
filter?: InputMaybe<Array<Scalars['String']>>;
};
export type ActorExperiment = {
__typename?: 'ActorExperiment';
createdAt: Scalars['DateTime'];
enabled: Scalars['Boolean'];
experiment: Experiment;
id: Scalars['ID'];
updatedAt: Scalars['DateTime'];
};
export type ActorExperimentMutation = {
__typename?: 'ActorExperimentMutation';
/** Create or update the value of a User Experiment */
createOrUpdateActorExperiment: ActorExperiment;
};
export type ActorExperimentMutationCreateOrUpdateActorExperimentArgs = {
enabled: Scalars['Boolean'];
experiment: Experiment;
};
export type ActorQuery = {
__typename?: 'ActorQuery';
/** Query an Actor by ID */
/**
* Query an Actor by ID
* @deprecated Public actor queries are no longer supported
*/
byId: Actor;
};
@@ -794,6 +814,7 @@ export type AndroidBuilderEnvironmentInput = {
image?: InputMaybe<Scalars['String']>;
ndk?: InputMaybe<Scalars['String']>;
node?: InputMaybe<Scalars['String']>;
pnpm?: InputMaybe<Scalars['String']>;
yarn?: InputMaybe<Scalars['String']>;
};
@@ -1023,6 +1044,7 @@ export type App = Project & {
insights: AppInsights;
/** iOS app credentials for the project */
iosAppCredentials: Array<IosAppCredentials>;
isDeleting: Scalars['Boolean'];
/** Whether the latest classic update publish is using a deprecated SDK version */
isDeprecated: Scalars['Boolean'];
/** @deprecated 'likes' have been deprecated. */
@@ -1057,6 +1079,8 @@ export type App = Project & {
releaseChannels: Array<Scalars['String']>;
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
/** Runtimes associated with this app */
runtimes: RuntimesConnection;
scopeKey: Scalars['String'];
/** SDK version of the latest classic update publish, 0.0.0 otherwise */
sdkVersion: Scalars['String'];
@@ -1174,6 +1198,7 @@ export type AppDeploymentArgs = {
export type AppDeploymentsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<DeploymentFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
@@ -1212,6 +1237,15 @@ export type AppLikedByArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppRuntimesArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppSubmissionsArgs = {
filter: SubmissionFilter;
@@ -2053,6 +2087,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
completedAt?: Maybe<Scalars['DateTime']>;
createdAt: Scalars['DateTime'];
customWorkflowName?: Maybe<Scalars['String']>;
developmentClient?: Maybe<Scalars['Boolean']>;
distribution?: Maybe<DistributionType>;
enqueuedAt?: Maybe<Scalars['DateTime']>;
error?: Maybe<BuildError>;
@@ -2269,6 +2304,7 @@ export type BuildMetadataInput = {
cliVersion?: InputMaybe<Scalars['String']>;
credentialsSource?: InputMaybe<BuildCredentialsSource>;
customWorkflowName?: InputMaybe<Scalars['String']>;
developmentClient?: InputMaybe<Scalars['Boolean']>;
distribution?: InputMaybe<DistributionType>;
gitCommitHash?: InputMaybe<Scalars['String']>;
gitCommitMessage?: InputMaybe<Scalars['String']>;
@@ -2771,8 +2807,10 @@ export type Deployment = {
builds: DeploymentBuildsConnection;
channel: UpdateChannel;
id: Scalars['ID'];
/** @deprecated Not required for the new Deployment UI */
latestUpdate?: Maybe<Update>;
/** Deployment query field */
insights: DeploymentInsights;
/** Ordered the same way as 'updateBranches' in UpdateChannel */
latestUpdatesPerBranch: Array<LatestUpdateOnBranch>;
runtime: Runtime;
};
@@ -2785,6 +2823,13 @@ export type DeploymentBuildsArgs = {
last?: InputMaybe<Scalars['Int']>;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentLatestUpdatesPerBranchArgs = {
limit: Scalars['Int'];
offset: Scalars['Int'];
};
export type DeploymentBuildEdge = {
__typename?: 'DeploymentBuildEdge';
cursor: Scalars['String'];
@@ -2804,6 +2849,40 @@ export type DeploymentEdge = {
node: Deployment;
};
export type DeploymentFilterInput = {
channel?: InputMaybe<Scalars['String']>;
runtimeVersion?: InputMaybe<Scalars['String']>;
};
export type DeploymentInsights = {
__typename?: 'DeploymentInsights';
embeddedUpdateTotalUniqueUsers: Scalars['Int'];
embeddedUpdateUniqueUsersOverTime: UniqueUsersOverTimeData;
id: Scalars['ID'];
mostPopularUpdates: Array<Update>;
uniqueUsersOverTime: UniqueUsersOverTimeData;
};
export type DeploymentInsightsEmbeddedUpdateTotalUniqueUsersArgs = {
timespan: InsightsTimespan;
};
export type DeploymentInsightsEmbeddedUpdateUniqueUsersOverTimeArgs = {
timespan: InsightsTimespan;
};
export type DeploymentInsightsMostPopularUpdatesArgs = {
timespan: InsightsTimespan;
};
export type DeploymentInsightsUniqueUsersOverTimeArgs = {
timespan: InsightsTimespan;
};
/** Represents the connection over the deployments edge of an App */
export type DeploymentsConnection = {
__typename?: 'DeploymentsConnection';
@@ -2972,6 +3051,10 @@ export type EstimatedUsage = {
value: Scalars['Float'];
};
export enum Experiment {
Orbit = 'ORBIT'
}
export type ExperimentationQuery = {
__typename?: 'ExperimentationQuery';
/** Get device experimentation config */
@@ -3542,6 +3625,7 @@ export type IosBuilderEnvironmentInput = {
fastlane?: InputMaybe<Scalars['String']>;
image?: InputMaybe<Scalars['String']>;
node?: InputMaybe<Scalars['String']>;
pnpm?: InputMaybe<Scalars['String']>;
yarn?: InputMaybe<Scalars['String']>;
};
@@ -3668,6 +3752,12 @@ export type KeystoreGenerationUrlMutation = {
createKeystoreGenerationUrl: KeystoreGenerationUrl;
};
export type LatestUpdateOnBranch = {
__typename?: 'LatestUpdateOnBranch';
branchId: Scalars['String'];
update?: Maybe<Update>;
};
export type LeaveAccountResult = {
__typename?: 'LeaveAccountResult';
success: Scalars['Boolean'];
@@ -3845,6 +3935,7 @@ export type MeteredBillingStatus = {
export type Notification = {
__typename?: 'Notification';
accountName: Scalars['String'];
createdAt: Scalars['DateTime'];
event: NotificationEvent;
id: Scalars['ID'];
@@ -3852,6 +3943,7 @@ export type Notification = {
metadata?: Maybe<NotificationMetadata>;
type: NotificationType;
updatedAt: Scalars['DateTime'];
websiteMessage: Scalars['String'];
};
export enum NotificationEvent {
@@ -4084,6 +4176,8 @@ export type Robot = Actor & {
accounts: Array<Account>;
created: Scalars['DateTime'];
displayName: Scalars['String'];
/** Experiments associated with this actor */
experiments: Array<ActorExperiment>;
/**
* Server feature gate values for this actor, optionally filtering by desired gates.
* Only resolves for the viewer.
@@ -4155,6 +4249,8 @@ export type RootMutation = {
account: AccountMutation;
/** Mutations that create, update, and delete an AccountSSOConfiguration */
accountSSOConfiguration: AccountSsoConfigurationMutation;
/** Mutations for Actor experiments */
actorExperiment: ActorExperimentMutation;
/** Mutations that modify the build credentials for an Android app */
androidAppBuildCredentials: AndroidAppBuildCredentialsMutation;
/** Mutations that modify the credentials for an Android app */
@@ -4230,6 +4326,8 @@ export type RootMutation = {
userInvitation: UserInvitationMutation;
/** Mutations that create, delete, update Webhooks */
webhook: WebhookMutation;
/** Mutations that modify a websiteNotification */
websiteNotifications: WebsiteNotificationMutation;
};
@@ -4263,7 +4361,10 @@ export type RootQuery = {
account: AccountQuery;
/** Top-level query object for querying AccountSSOConfigurationPublicData */
accountSSOConfigurationPublicData: AccountSsoConfigurationPublicDataQuery;
/** Top-level query object for querying Actors. */
/**
* Top-level query object for querying Actors.
* @deprecated Public actor queries are no longer supported
*/
actor: ActorQuery;
/**
* Public apps in the app directory
@@ -4320,7 +4421,10 @@ export type RootQuery = {
* @deprecated Public user queries are no longer supported
*/
user: UserQuery;
/** Top-level query object for querying UserActors. */
/**
* Top-level query object for querying UserActors.
* @deprecated Public user queries are no longer supported
*/
userActor: UserActorQuery;
/** Top-level query object for querying UserActorPublicData publicly. */
userActorPublicData: UserActorPublicDataQuery;
@@ -4386,6 +4490,7 @@ export type RuntimeFilterInput = {
branchId?: InputMaybe<Scalars['String']>;
};
/** Represents the connection over the runtime edge of an App */
export type RuntimesConnection = {
__typename?: 'RuntimesConnection';
edges: Array<RuntimeEdge>;
@@ -4410,6 +4515,8 @@ export type SsoUser = Actor & UserActor & {
/** Discord account linked to a user */
discordUser?: Maybe<DiscordUser>;
displayName: Scalars['String'];
/** Experiments associated with this actor */
experiments: Array<ActorExperiment>;
/**
* Server feature gate values for this actor, optionally filtering by desired gates.
* Only resolves for the viewer.
@@ -4437,8 +4544,9 @@ export type SsoUser = Actor & UserActor & {
/** @deprecated No longer supported */
twitterUsername?: Maybe<Scalars['String']>;
username: Scalars['String'];
/** Web notifications linked to a user */
/** @deprecated No longer supported */
websiteNotifications: Array<Notification>;
websiteNotificationsPaginated: WebsiteNotificationsConnection;
};
@@ -4476,6 +4584,15 @@ export type SsoUserSnacksArgs = {
offset: Scalars['Int'];
};
/** Represents a human SSO (not robot) actor. */
export type SsoUserWebsiteNotificationsPaginatedArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
export type SsoUserDataInput = {
firstName?: InputMaybe<Scalars['String']>;
lastName?: InputMaybe<Scalars['String']>;
@@ -4670,7 +4787,9 @@ export type StatuspageService = {
export enum StatuspageServiceName {
EasBuild = 'EAS_BUILD',
EasSubmit = 'EAS_SUBMIT',
EasUpdate = 'EAS_UPDATE'
EasUpdate = 'EAS_UPDATE',
GithubApiRequests = 'GITHUB_API_REQUESTS',
GithubWebhooks = 'GITHUB_WEBHOOKS'
}
export type StatuspageServiceQuery = {
@@ -4904,6 +5023,8 @@ export type Update = ActivityTimelineProjectActivity & {
gitCommitHash?: Maybe<Scalars['String']>;
group: Scalars['String'];
id: Scalars['ID'];
/** Update query field */
insights: UpdateInsights;
isGitWorkingTreeDirty: Scalars['Boolean'];
isRollBackToEmbedded: Scalars['Boolean'];
manifestFragment: Scalars['String'];
@@ -5064,6 +5185,17 @@ export type UpdateInfoGroup = {
web?: InputMaybe<PartialManifest>;
};
export type UpdateInsights = {
__typename?: 'UpdateInsights';
id: Scalars['ID'];
totalUniqueUsers: Scalars['Int'];
};
export type UpdateInsightsTotalUniqueUsersArgs = {
timespan: InsightsTimespan;
};
export type UpdateMutation = {
__typename?: 'UpdateMutation';
/** Delete an EAS update group */
@@ -5163,6 +5295,8 @@ export type User = Actor & UserActor & {
displayName: Scalars['String'];
email: Scalars['String'];
emailVerified: Scalars['Boolean'];
/** Experiments associated with this actor */
experiments: Array<ActorExperiment>;
/**
* Server feature gate values for this actor, optionally filtering by desired gates.
* Only resolves for the viewer.
@@ -5199,7 +5333,9 @@ export type User = Actor & UserActor & {
/** @deprecated No longer supported */
twitterUsername?: Maybe<Scalars['String']>;
username: Scalars['String'];
/** @deprecated No longer supported */
websiteNotifications: Array<Notification>;
websiteNotificationsPaginated: WebsiteNotificationsConnection;
};
@@ -5237,6 +5373,15 @@ export type UserSnacksArgs = {
offset: Scalars['Int'];
};
/** Represents a human (not robot) actor. */
export type UserWebsiteNotificationsPaginatedArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */
export type UserActor = {
/** Access Tokens belonging to this user actor */
@@ -5261,6 +5406,8 @@ export type UserActor = {
* For example, when displaying a sentence indicating that actor X created a build or published an update.
*/
displayName: Scalars['String'];
/** Experiments associated with this actor */
experiments: Array<ActorExperiment>;
/**
* Server feature gate values for this user actor, optionally filtering by desired gates.
* Only resolves for the viewer.
@@ -5288,8 +5435,9 @@ export type UserActor = {
/** @deprecated No longer supported */
twitterUsername?: Maybe<Scalars['String']>;
username: Scalars['String'];
/** Web notifications linked to a user */
/** @deprecated No longer supported */
websiteNotifications: Array<Notification>;
websiteNotificationsPaginated: WebsiteNotificationsConnection;
};
@@ -5327,6 +5475,15 @@ export type UserActorSnacksArgs = {
offset: Scalars['Int'];
};
/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */
export type UserActorWebsiteNotificationsPaginatedArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */
export type UserActorPublicData = {
__typename?: 'UserActorPublicData';
@@ -5369,9 +5526,15 @@ export type UserActorPublicDataQueryByUsernameArgs = {
export type UserActorQuery = {
__typename?: 'UserActorQuery';
/** Query a UserActor by ID */
/**
* Query a UserActor by ID
* @deprecated Public user actor queries are no longer supported
*/
byId: UserActor;
/** Query a UserActor by username */
/**
* Query a UserActor by username
* @deprecated Public user actor queries are no longer supported
*/
byUsername: UserActor;
};
@@ -5544,6 +5707,11 @@ export type UserSecondFactorDevice = {
user: User;
};
export type WebNotificationUpdateReadStateInput = {
id: Scalars['ID'];
isRead: Scalars['Boolean'];
};
export type Webhook = {
__typename?: 'Webhook';
appId: Scalars['ID'];
@@ -5606,6 +5774,29 @@ export enum WebhookType {
Submit = 'SUBMIT'
}
export type WebsiteNotificationEdge = {
__typename?: 'WebsiteNotificationEdge';
cursor: Scalars['String'];
node: Notification;
};
export type WebsiteNotificationMutation = {
__typename?: 'WebsiteNotificationMutation';
updateAllWebsiteNotificationReadStateAsRead: Scalars['Boolean'];
updateNotificationReadState: Notification;
};
export type WebsiteNotificationMutationUpdateNotificationReadStateArgs = {
input: WebNotificationUpdateReadStateInput;
};
export type WebsiteNotificationsConnection = {
__typename?: 'WebsiteNotificationsConnection';
edges: Array<WebsiteNotificationEdge>;
pageInfo: PageInfo;
};
export type DeleteAndroidAppBuildCredentialsResult = {
__typename?: 'deleteAndroidAppBuildCredentialsResult';
id: Scalars['ID'];
@@ -6334,7 +6525,7 @@ export type ViewUpdateChannelOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> } | null } } };
export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -6343,7 +6534,7 @@ export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
}>;
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> }> } } };
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> }> }> } } };
export type ViewUpdateChannelsPaginatedOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -60,6 +60,7 @@ export const ChannelQuery = {
updateChannelByName(name: $channelName) {
id
name
updatedAt
createdAt
branchMapping
updateBranches(offset: 0, limit: 5) {
@@ -104,6 +105,7 @@ export const ChannelQuery = {
updateChannels(offset: $offset, limit: $limit) {
id
name
updatedAt
createdAt
branchMapping
updateBranches(offset: 0, limit: 5) {
+4
View File
@@ -57,6 +57,10 @@ export default class Log {
Log.consoleLog(chalk.green(figures.tick), ...args);
}
public static withInfo(...args: any[]): void {
Log.consoleLog(chalk.green(figures.info), ...args);
}
private static consoleLog(...args: any[]): void {
Log.updateIsLastLineNewLine(args);
// eslint-disable-next-line no-console
+55 -18
View File
@@ -25,7 +25,11 @@ import { promptAsync } from '../prompts';
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../update/utils';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import {
expoCommandAsync,
shouldUseVersionedExpoCLI,
shouldUseVersionedExpoCLIWithExplicitPlatforms,
} from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import { truthy } from '../utils/expodash/filter';
import uniqBy from '../utils/expodash/uniqBy';
@@ -184,7 +188,7 @@ export async function buildBundlesAsync({
}: {
projectDir: string;
inputDir: string;
exp: Pick<ExpoConfig, 'sdkVersion'>;
exp: Pick<ExpoConfig, 'sdkVersion' | 'web'>;
platformFlag: ExpoCLIExportPlatformFlag;
clearCache?: boolean;
}): Promise<void> {
@@ -193,20 +197,9 @@ export async function buildBundlesAsync({
throw new Error('Could not locate package.json');
}
if (shouldUseVersionedExpoCLI(projectDir, exp)) {
await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--dump-sourcemap',
'--dump-assetmap',
'--platform',
platformFlag,
...(clearCache ? ['--clear'] : []),
]);
} else {
// Legacy global Expo CLI
await expoCommandAsync(projectDir, [
// Legacy global Expo CLI
if (!shouldUseVersionedExpoCLI(projectDir, exp)) {
return await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
@@ -214,11 +207,49 @@ export async function buildBundlesAsync({
'--non-interactive',
'--dump-sourcemap',
'--dump-assetmap',
'--platform',
platformFlag,
`--platform=${platformFlag}`,
...(clearCache ? ['--clear'] : []),
]);
}
// Versioned Expo CLI, with multiple platform flag support
if (shouldUseVersionedExpoCLIWithExplicitPlatforms(projectDir)) {
// When creating EAS updates, we don't want to build a web bundle
const platformArgs =
platformFlag === 'all'
? ['--platform', 'ios', '--platform', 'android']
: ['--platform', platformFlag];
return await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--dump-sourcemap',
'--dump-assetmap',
...platformArgs,
...(clearCache ? ['--clear'] : []),
]);
}
// Versioned Expo CLI, without multiple platform flag support
// Warn users about potential export issues when using Metro web
// See: https://github.com/expo/expo/pull/23621
if (exp.web?.bundler === 'metro') {
Log.warn('Exporting bundle for all platforms, including Metro web.');
Log.warn(
'If your app is incompatible with web, remove the "expo.web.bundler" property from your app manifest, or upgrade to the latest Expo SDK.'
);
}
return await expoCommandAsync(projectDir, [
'export',
'--output-dir',
inputDir,
'--dump-sourcemap',
'--dump-assetmap',
`--platform=${platformFlag}`,
...(clearCache ? ['--clear'] : []),
]);
}
export async function resolveInputDirectoryAsync(
@@ -682,3 +713,9 @@ export function getRuntimeToPlatformMappingFromRuntimeVersions(
}
return runtimeToPlatformMapping;
}
export const platformDisplayNames: Record<Platform, string> = {
android: 'Android',
ios: 'iOS',
web: 'Web',
};
@@ -1,10 +1,11 @@
import { v4 as uuidv4 } from 'uuid';
import {
testChannelObject,
testUpdateBranch1,
testUpdateBranch2,
} from '../../channel/__tests__/fixtures';
channelInfoWithBranches,
testBasicBranchInfo1,
testBasicBranchInfo2,
testChannelBasicInfo,
} from '../../channel/__tests__/branch-mapping-fixtures';
import {
BranchMappingOperator,
BranchMappingValidationError,
@@ -17,6 +18,7 @@ import {
assertRolloutBranchMapping,
composeRollout,
createRolloutBranchMapping,
doesTargetRollout,
editRolloutBranchMapping,
getRollout,
getRolloutInfo,
@@ -32,18 +34,29 @@ import {
standardBranchMapping,
} from './fixtures';
describe(doesTargetRollout, () => {
it('detects whether a runtime targets a constrained rollout', () => {
expect(doesTargetRollout(rolloutBranchMapping, '1.0.0')).toBe(true);
expect(doesTargetRollout(rolloutBranchMapping, '2.0.0')).toBe(false);
});
it('should always return true if the rollout is unconstrained', () => {
expect(doesTargetRollout(rolloutBranchMappingLegacy, '1.0.0')).toBe(true);
expect(doesTargetRollout(rolloutBranchMappingLegacy, '2.0.0')).toBe(true);
});
});
describe(composeRollout, () => {
it('composes a rollout', () => {
const rollout = getRollout(testChannelObject);
const rolloutInfo = getRolloutInfo(testChannelObject);
const composedRollout = composeRollout(rolloutInfo, testUpdateBranch2, testUpdateBranch1);
const rollout = getRollout(channelInfoWithBranches);
const rolloutInfo = getRolloutInfo(testChannelBasicInfo);
const composedRollout = composeRollout(rolloutInfo, testBasicBranchInfo2, testBasicBranchInfo1);
expect(composedRollout).toEqual(rollout);
});
it('throws if the branches to not match the rollout info', () => {
const rolloutInfo = getRolloutInfo(testChannelObject);
expect(() => composeRollout(rolloutInfo, testUpdateBranch1, testUpdateBranch2)).toThrowError(
BranchMappingValidationError
);
const rolloutInfo = getRolloutInfo(testChannelBasicInfo);
expect(() =>
composeRollout(rolloutInfo, testBasicBranchInfo1, testBasicBranchInfo2)
).toThrowError(BranchMappingValidationError);
});
});
@@ -119,33 +132,33 @@ describe(isConstrainedRollout, () => {
describe(getRollout, () => {
it('doesnt get the mapping if it isnt a rollout', () => {
const notRollout = { ...testChannelObject };
const notRollout = { ...channelInfoWithBranches };
notRollout.branchMapping = JSON.stringify(standardBranchMapping);
expect(() => getRollout(notRollout)).toThrowError();
});
it('gets a constrained rollout', () => {
const constrainedRollout = { ...testChannelObject };
const constrainedRollout = { ...channelInfoWithBranches };
const rolloutBranchMappingWithCorrectBranch = { ...rolloutBranchMapping };
rolloutBranchMappingWithCorrectBranch.data[0].branchId = testUpdateBranch1.id;
rolloutBranchMappingWithCorrectBranch.data[1].branchId = testUpdateBranch2.id;
rolloutBranchMappingWithCorrectBranch.data[0].branchId = testBasicBranchInfo1.id;
rolloutBranchMappingWithCorrectBranch.data[1].branchId = testBasicBranchInfo2.id;
constrainedRollout.branchMapping = JSON.stringify(rolloutBranchMapping);
const rollout = getRollout(constrainedRollout);
expect(rollout.percentRolledOut).toEqual(10);
expect(isConstrainedRolloutInfo(rollout)).toBe(true);
expect(rollout.defaultBranch).toEqual(testUpdateBranch2);
expect(rollout.rolledOutBranch).toEqual(testUpdateBranch1);
expect(rollout.defaultBranchId).toEqual(testUpdateBranch2.id);
expect(rollout.rolledOutBranchId).toEqual(testUpdateBranch1.id);
expect(rollout.defaultBranch).toEqual(testBasicBranchInfo2);
expect(rollout.rolledOutBranch).toEqual(testBasicBranchInfo1);
expect(rollout.defaultBranchId).toEqual(testBasicBranchInfo2.id);
expect(rollout.rolledOutBranchId).toEqual(testBasicBranchInfo1.id);
});
it('gets a legacy rollout', () => {
const rollout = getRollout(testChannelObject);
const rollout = getRollout(channelInfoWithBranches);
expect(rollout.percentRolledOut).toEqual(15);
expect(isLegacyRolloutInfo(rollout)).toBe(true);
expect(rollout.defaultBranch).toEqual(testUpdateBranch2);
expect(rollout.rolledOutBranch).toEqual(testUpdateBranch1);
expect(rollout.defaultBranchId).toEqual(testUpdateBranch2.id);
expect(rollout.rolledOutBranchId).toEqual(testUpdateBranch1.id);
expect(rollout.defaultBranch).toEqual(testBasicBranchInfo2);
expect(rollout.rolledOutBranch).toEqual(testBasicBranchInfo1);
expect(rollout.defaultBranchId).toEqual(testBasicBranchInfo2.id);
expect(rollout.rolledOutBranchId).toEqual(testBasicBranchInfo1.id);
});
});
@@ -68,3 +68,8 @@ export const standardBranchMapping: BranchMapping = {
version: 0,
data: [{ branchId: uuidv4(), branchMappingLogic: alwaysTrue() }],
};
export const emptyBranchMapping: BranchMapping = {
version: 0,
data: [],
};
@@ -2,7 +2,11 @@ import { Updates } from '@expo/config-plugins';
import assert from 'assert';
import { SelectBranch } from '../../branch/actions/SelectBranch';
import { getStandardBranchId, hasStandardBranchMap } from '../../channel/branch-mapping';
import {
getStandardBranchId,
hasEmptyBranchMap,
hasStandardBranchMap,
} from '../../channel/branch-mapping';
import { getUpdateBranch } from '../../channel/utils';
import { updateChannelBranchMappingAsync } from '../../commands/channel/edit';
import { EASUpdateAction, EASUpdateContext } from '../../eas-update/utils';
@@ -71,6 +75,11 @@ export class CreateRollout implements EASUpdateAction<UpdateChannelBasicInfoFrag
if (isRollout(this.channelInfo)) {
throw new Error(`A rollout is already in progress for channel ${this.channelInfo.name}`);
}
if (hasEmptyBranchMap(this.channelInfo)) {
throw new Error(
`Your channel needs to be linked to a branch before a rollout can be created. Do this by running 'eas channel:edit'`
);
}
if (!hasStandardBranchMap(this.channelInfo)) {
throw new Error(
`You have a custom branch mapping. Map your channel to a single branch before creating a rollout. Received: ${this.channelInfo.branchMapping}`
@@ -5,7 +5,11 @@ import { getAlwaysTrueBranchMapping } from '../../channel/branch-mapping';
import { updateChannelBranchMappingAsync } from '../../commands/channel/edit';
import { EASUpdateAction, EASUpdateContext } from '../../eas-update/utils';
import { UpdateChannelBasicInfoFragment } from '../../graphql/generated';
import { ChannelQuery, UpdateChannelObject } from '../../graphql/queries/ChannelQuery';
import {
ChannelQuery,
UpdateBranchObject,
UpdateChannelObject,
} from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
import { republishAsync } from '../../update/republish';
@@ -103,7 +107,7 @@ export class EndRollout implements EASUpdateAction<UpdateChannelBasicInfoFragmen
});
}
async selectOutcomeAsync(rollout: Rollout): Promise<EndOutcome> {
async selectOutcomeAsync(rollout: Rollout<UpdateBranchObject>): Promise<EndOutcome> {
const { rolledOutBranch, percentRolledOut, defaultBranch } = rollout;
const rolledOutUpdateGroup = rolledOutBranch.updateGroups[0];
const defaultUpdateGroup = defaultBranch.updateGroups[0];
@@ -142,7 +146,7 @@ export class EndRollout implements EASUpdateAction<UpdateChannelBasicInfoFragmen
async performOutcomeAsync(
ctx: EASUpdateContext,
rollout: Rollout,
rollout: Rollout<UpdateBranchObject>,
outcome: EndOutcome
): Promise<UpdateChannelBasicInfoFragment> {
const { graphqlClient, app } = ctx;
@@ -184,7 +188,7 @@ export class EndRollout implements EASUpdateAction<UpdateChannelBasicInfoFragmen
async confirmOutcomeAsync(
ctx: EASUpdateContext,
selectedOutcome: EndOutcome,
rollout: Rollout
rollout: Rollout<UpdateBranchObject>
): Promise<boolean> {
const { nonInteractive } = ctx;
if (nonInteractive) {
@@ -20,6 +20,7 @@ import {
export enum ManageRolloutActions {
EDIT = 'Edit',
END = 'End',
VIEW = 'View',
GO_BACK = 'Go back',
}
@@ -31,7 +32,7 @@ export class ManageRollout implements EASUpdateAction<EASUpdateAction> {
private channelInfo: UpdateChannelBasicInfoFragment,
private options: {
callingAction?: EASUpdateAction;
action?: ManageRolloutActions.EDIT | ManageRolloutActions.END;
action?: ManageRolloutActions.EDIT | ManageRolloutActions.END | ManageRolloutActions.VIEW;
} & Partial<EditRolloutNonInteractiveOptions> &
Partial<EndRolloutNonInteractiveOptions> &
EndRolloutGeneralOptions
@@ -51,6 +52,9 @@ export class ManageRollout implements EASUpdateAction<EASUpdateAction> {
return new EditRollout(this.channelInfo, this.options);
case ManageRolloutActions.END:
return new EndRollout(this.channelInfo, this.options);
case ManageRolloutActions.VIEW:
// Rollout is automatically printed in interactive mode
return new Noop();
case ManageRolloutActions.GO_BACK:
assert(this.options.callingAction, 'calling action must be defined');
return this.options.callingAction;
@@ -94,3 +98,7 @@ export class ManageRollout implements EASUpdateAction<EASUpdateAction> {
});
}
}
class Noop {
public async runAsync(): Promise<void> {}
}
@@ -1,7 +1,14 @@
import { EASUpdateAction, EASUpdateContext } from '../../eas-update/utils';
import { UpdateChannelBasicInfoFragment } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { CodeSigningInfo } from '../../utils/code-signing';
import {
ChannelQuery,
UpdateBranchObject,
UpdateChannelObject,
} from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { printJsonOnlyOutput } from '../../utils/json';
import { getRollout, getRolloutInfo, isConstrainedRolloutInfo, isRollout } from '../branch-mapping';
import { printRollout } from '../utils';
import {
CreateRollout,
NonInteractiveOptions as CreateRolloutNonInteractiveOptions,
@@ -18,6 +25,19 @@ import {
import { ManageRolloutActions } from './ManageRollout';
import { MainMenuActions, RolloutActions } from './RolloutMainMenu';
type JSONRolloutOutput = {
defaultBranch: UpdateBranchObject;
rolledOutBranch: UpdateBranchObject;
percentRolledOut: number;
runtimeVersion?: string;
updatedAt: Date;
};
type JSONOutput = {
hasRollout: boolean;
originalRolloutInfo?: JSONRolloutOutput;
currentRolloutInfo?: JSONRolloutOutput;
};
/**
* Control a rollout in non interactive mode.
*/
@@ -25,7 +45,7 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
constructor(
private options: {
channelName?: string;
codeSigningInfo?: CodeSigningInfo;
json?: boolean;
action?: RolloutActions;
} & Partial<EditRolloutNonInteractiveOptions> &
Partial<EndRolloutNonInteractiveOptions> &
@@ -49,29 +69,102 @@ export class NonInteractiveRollout implements EASUpdateAction<void> {
appId: app.projectId,
channelName,
});
const channelObject = await this.getChannelObjectAsync(ctx, {
channelName,
runtimeVersion: this.getRuntimeVersion(channelInfo),
});
if (!action) {
throw new Error(`--action is required in non-interactive mode.`);
}
await this.runActionAsync(ctx, action, channelInfo, this.options);
const updatedChannelInfo = await this.runActionAsync(ctx, action, channelObject);
const updatedChannelObject = await this.getChannelObjectAsync(ctx, {
channelName,
runtimeVersion: this.getRuntimeVersion(updatedChannelInfo),
});
if (this.options.json) {
const json = await this.getJsonAsync({
originalChannelObject: channelObject,
updatedChannelObject,
});
printJsonOnlyOutput(json);
}
}
async runActionAsync(
ctx: EASUpdateContext,
action: RolloutActions,
channelInfo: UpdateChannelBasicInfoFragment,
options: Partial<EditRolloutNonInteractiveOptions> &
Partial<EndRolloutNonInteractiveOptions> &
EndRolloutGeneralOptions &
Partial<CreateRolloutNonInteractiveOptions>
channelObject: UpdateChannelObject
): Promise<UpdateChannelBasicInfoFragment> {
switch (action) {
case MainMenuActions.CREATE_NEW:
return await new CreateRollout(channelInfo, options).runAsync(ctx);
return await new CreateRollout(channelObject, this.options).runAsync(ctx);
case ManageRolloutActions.EDIT:
return await new EditRollout(channelInfo, options).runAsync(ctx);
return await new EditRollout(channelObject, this.options).runAsync(ctx);
case ManageRolloutActions.END:
return await new EndRollout(channelInfo, options).runAsync(ctx);
return await new EndRollout(channelObject, this.options).runAsync(ctx);
case ManageRolloutActions.VIEW:
return this.viewRollout(channelObject);
}
}
viewRollout(channelObject: UpdateChannelObject): UpdateChannelObject {
if (!this.options.json) {
printRollout(channelObject);
Log.warn('For formatted output, add the --json flag to your command.');
}
return channelObject;
}
async getJsonAsync({
originalChannelObject,
updatedChannelObject,
}: {
originalChannelObject: UpdateChannelObject;
updatedChannelObject: UpdateChannelObject;
}): Promise<JSONOutput> {
return {
hasRollout: isRollout(updatedChannelObject),
...(isRollout(originalChannelObject)
? { originalRolloutInfo: await this.getRolloutJsonAsync(originalChannelObject) }
: {}),
...(isRollout(updatedChannelObject)
? { currentRolloutInfo: await this.getRolloutJsonAsync(updatedChannelObject) }
: {}),
};
}
async getRolloutJsonAsync(channelObject: UpdateChannelObject): Promise<JSONRolloutOutput> {
const rollout = getRollout(channelObject);
return {
defaultBranch: rollout.defaultBranch,
rolledOutBranch: rollout.rolledOutBranch,
percentRolledOut: rollout.percentRolledOut,
runtimeVersion: this.getRuntimeVersion(channelObject) ?? undefined,
updatedAt: channelObject.updatedAt,
};
}
getRuntimeVersion(channelInfo: UpdateChannelBasicInfoFragment): string | undefined {
if (isRollout(channelInfo)) {
const updatedRolloutInfo = getRolloutInfo(channelInfo);
if (isConstrainedRolloutInfo(updatedRolloutInfo)) {
return updatedRolloutInfo.runtimeVersion;
}
}
return undefined;
}
async getChannelObjectAsync(
ctx: EASUpdateContext,
{ channelName, runtimeVersion }: { channelName: string; runtimeVersion?: string }
): Promise<UpdateChannelObject> {
const { graphqlClient, app } = ctx;
const { projectId } = app;
return await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName,
...(runtimeVersion ? { filter: { runtimeVersions: [runtimeVersion] } } : {}),
});
}
}
@@ -27,7 +27,8 @@ export enum MainMenuActions {
export type RolloutActions =
| MainMenuActions.CREATE_NEW
| ManageRolloutActions.EDIT
| ManageRolloutActions.END;
| ManageRolloutActions.END
| ManageRolloutActions.VIEW;
/**
* Manage a rollout for the project.
@@ -126,7 +127,11 @@ export class RolloutMainMenu implements EASUpdateAction<void> {
toMainMenuAction(action: RolloutActions): MainMenuActions {
if (action === MainMenuActions.CREATE_NEW) {
return MainMenuActions.CREATE_NEW;
} else if (action === ManageRolloutActions.EDIT || action === ManageRolloutActions.END) {
} else if (
action === ManageRolloutActions.EDIT ||
action === ManageRolloutActions.END ||
action === ManageRolloutActions.VIEW
) {
return MainMenuActions.MANAGE_EXISTING;
} else {
throw new Error(`Action not supported yet: ` + action);
@@ -0,0 +1,65 @@
import { testBasicChannelInfo, testChannelObject } from '../../../channel/__tests__/fixtures';
import { getAlwaysTrueBranchMapping, getBranchMapping } from '../../../channel/branch-mapping';
import { updateChannelBranchMappingAsync } from '../../../commands/channel/edit';
import { createCtxMock } from '../../../eas-update/__tests__/fixtures';
import { ChannelQuery } from '../../../graphql/queries/ChannelQuery';
import { confirmAsync } from '../../../prompts';
import { RolloutBranchMapping, editRolloutBranchMapping } from '../../branch-mapping';
import { EditRollout } from '../EditRollout';
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/ChannelQuery');
jest.mock('../../../commands/channel/edit');
describe(EditRollout, () => {
afterEach(() => {
jest.clearAllMocks();
});
it('throws if the channel doesnt have an active rollout', async () => {
const ctx = createCtxMock();
const editRollout = new EditRollout({
...testBasicChannelInfo,
branchMapping: JSON.stringify(getAlwaysTrueBranchMapping('test-branch')),
});
await expect(editRollout.runAsync(ctx)).rejects.toThrowError('is not a rollout');
});
it('edits the channel with an active rollout', async () => {
const ctx = createCtxMock();
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockResolvedValue(testChannelObject);
jest.mocked(updateChannelBranchMappingAsync).mockResolvedValue(testChannelObject);
jest.mocked(confirmAsync).mockImplementation(async () => true);
const editRollout = new EditRollout(testChannelObject, { percent: 50 });
await editRollout.runAsync(ctx);
expect(updateChannelBranchMappingAsync).toBeCalledWith(
expect.any(Function),
expect.objectContaining({
branchMapping: JSON.stringify(
editRolloutBranchMapping(
getBranchMapping(testChannelObject.branchMapping) as RolloutBranchMapping,
50
)
),
})
);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockResolvedValue(testChannelObject);
jest.mocked(updateChannelBranchMappingAsync).mockResolvedValue(testChannelObject);
jest.mocked(confirmAsync).mockImplementation(async () => true);
const editRollout = new EditRollout(testChannelObject, { percent: 50 });
await editRollout.runAsync(ctx);
expect(updateChannelBranchMappingAsync).toBeCalledWith(
expect.any(Function),
expect.objectContaining({
branchMapping: JSON.stringify(
editRolloutBranchMapping(
getBranchMapping(testChannelObject.branchMapping) as RolloutBranchMapping,
50
)
),
})
);
});
});
@@ -0,0 +1,96 @@
import { testUpdateBranch1, testUpdateBranch2 } from '../../../channel/__tests__/fixtures';
import { createCtxMock } from '../../../eas-update/__tests__/fixtures';
import { NonInteractiveError } from '../../../eas-update/utils';
import { RuntimeQuery } from '../../../graphql/queries/RuntimeQuery';
import { UpdateQuery } from '../../../graphql/queries/UpdateQuery';
import { confirmAsync, promptAsync } from '../../../prompts';
import { SelectRuntime } from '../SelectRuntime';
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/RuntimeQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
const runtimeFragment1 = { id: 'test-runtime-1-id', version: '1.0.0' };
const runtimeFragment2 = { id: 'test-runtime-2-id', version: '2.0.0' };
describe(SelectRuntime, () => {
afterEach(() => {
jest.clearAllMocks();
});
it('returns null if project has no runtimes', async () => {
const ctx = createCtxMock();
jest.mocked(RuntimeQuery.getRuntimesOnBranchAsync).mockResolvedValue({
edges: [],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const selectRuntime = new SelectRuntime(testUpdateBranch1);
const runtime = await selectRuntime.runAsync(ctx);
expect(runtime).toBeNull();
});
it('still prompts if project has only one runtime', async () => {
const ctx = createCtxMock();
jest.mocked(RuntimeQuery.getRuntimesOnBranchAsync).mockResolvedValue({
edges: [{ node: runtimeFragment1, cursor: 'cursor' }],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(UpdateQuery.viewUpdateGroupsOnBranchAsync).mockResolvedValue([]);
jest.mocked(confirmAsync).mockImplementation(async () => true);
const selectRuntime = new SelectRuntime(testUpdateBranch1);
const runtime = await selectRuntime.runAsync(ctx);
expect(runtime).toBe('1.0.0');
expect(confirmAsync).toHaveBeenCalledTimes(1);
});
it('returns the selected runtime', async () => {
jest.mocked(RuntimeQuery.getRuntimesOnBranchAsync).mockResolvedValue({
edges: [
{ node: runtimeFragment1, cursor: 'cursor' },
{ node: runtimeFragment2, cursor: 'cursor' },
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
item: runtimeFragment2,
}));
const ctx = createCtxMock();
const selectRuntime = new SelectRuntime(testUpdateBranch1);
const runtime = await selectRuntime.runAsync(ctx);
expect(runtime).toBe('2.0.0');
});
it('filters the runtime by intersecting branch id', async () => {
const ctx = createCtxMock();
jest.mocked(RuntimeQuery.getRuntimesOnBranchAsync).mockResolvedValue({
edges: [],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const selectRuntime = new SelectRuntime(testUpdateBranch1, {
anotherBranchToIntersectRuntimesBy: testUpdateBranch2,
});
const runtime = await selectRuntime.runAsync(ctx);
expect(runtime).toBeNull();
expect(RuntimeQuery.getRuntimesOnBranchAsync).toBeCalledWith(
expect.any(Function),
expect.objectContaining({
filter: { branchId: testUpdateBranch2.id },
})
);
});
it('throws an error in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const selectRuntime = new SelectRuntime(testUpdateBranch1);
// dont fail if users are running in non-interactive mode
await expect(selectRuntime.runAsync(ctx)).rejects.toThrowError(NonInteractiveError);
});
});
+47 -22
View File
@@ -1,5 +1,3 @@
import assert from 'assert';
import {
BranchMapping,
BranchMappingAlwaysTrue,
@@ -16,19 +14,24 @@ import {
isAndStatement,
isStatement,
} from '../channel/branch-mapping';
import { getUpdateBranch } from '../channel/utils';
import { UpdateChannelBasicInfoFragment } from '../graphql/generated';
import { UpdateBranchObject, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import {
BranchBasicInfo,
ChannelBasicInfo,
UpdateChannelInfoWithBranches,
getUpdateBranch,
} from '../channel/utils';
export type Rollout = LegacyRollout | ConstrainedRollout;
export type Rollout<Branch extends BranchBasicInfo> =
| LegacyRollout<Branch>
| ConstrainedRollout<Branch>;
export type RolloutInfo = LegacyRolloutInfo | ConstrainedRolloutInfo;
type ConstrainedRollout = LegacyRollout & {
type ConstrainedRollout<Branch extends BranchBasicInfo> = LegacyRollout<Branch> & {
runtimeVersion: string;
};
type LegacyRollout = {
rolledOutBranch: UpdateBranchObject;
defaultBranch: UpdateBranchObject;
type LegacyRollout<Branch extends BranchBasicInfo> = {
rolledOutBranch: Branch;
defaultBranch: Branch;
} & LegacyRolloutInfo;
type ConstrainedRolloutInfo = LegacyRolloutInfo & {
@@ -95,11 +98,13 @@ export function isConstrainedRolloutInfo(rollout: RolloutInfo): rollout is Const
return 'runtimeVersion' in rollout;
}
export function isConstrainedRollout(rollout: Rollout): rollout is ConstrainedRollout {
export function isConstrainedRollout<Branch extends BranchBasicInfo>(
rollout: Rollout<Branch>
): rollout is ConstrainedRollout<Branch> {
return isConstrainedRolloutInfo(rollout);
}
export function getRolloutInfo(basicChannelInfo: UpdateChannelBasicInfoFragment): RolloutInfo {
export function getRolloutInfo(basicChannelInfo: ChannelBasicInfo): RolloutInfo {
const rolloutBranchMapping = getRolloutBranchMapping(basicChannelInfo.branchMapping);
return getRolloutInfoFromBranchMapping(rolloutBranchMapping);
}
@@ -114,13 +119,18 @@ export function getRolloutInfoFromBranchMapping(branchMapping: RolloutBranchMapp
const nodesFromStatement = getNodesFromStatement(statementNode);
const runtimeVersionNode = nodesFromStatement.find(isRuntimeVersionNode);
assert(runtimeVersionNode, 'Runtime version node must be defined.');
if (!runtimeVersionNode) {
throw new BranchMappingValidationError('Runtime version node must be defined.');
}
assertNodeObject(runtimeVersionNode);
const runtimeVersion = runtimeVersionNode.operand;
assertString(runtimeVersion);
const rolloutNode = nodesFromStatement.find(isRolloutNode);
assert(rolloutNode, 'Rollout node must be defined.');
if (!rolloutNode) {
throw new BranchMappingValidationError('Rollout node must be defined.');
}
assertNodeObject(rolloutNode);
const operand = rolloutNode.operand;
assertNumber(operand);
@@ -143,21 +153,23 @@ export function getRolloutInfoFromBranchMapping(branchMapping: RolloutBranchMapp
}
}
export function getRollout(channel: UpdateChannelObject): Rollout {
export function getRollout<Branch extends BranchBasicInfo>(
channel: UpdateChannelInfoWithBranches<Branch>
): Rollout<Branch> {
const rolloutBranchMapping = getRolloutBranchMapping(channel.branchMapping);
const rolledOutBranchId = rolloutBranchMapping.data[0].branchId;
const rolledOutBranch = getUpdateBranch(channel, rolledOutBranchId);
const defaultBranchId = rolloutBranchMapping.data[1].branchId;
const defaultBranch = getUpdateBranch(channel, defaultBranchId);
const rolloutInfo = getRolloutInfo(channel);
return composeRollout(rolloutInfo, defaultBranch, rolledOutBranch);
return composeRollout<Branch>(rolloutInfo, defaultBranch, rolledOutBranch);
}
export function composeRollout(
export function composeRollout<Branch extends BranchBasicInfo>(
rolloutInfo: RolloutInfo,
defaultBranch: UpdateBranchObject,
rolledOutBranch: UpdateBranchObject
): Rollout {
defaultBranch: Branch,
rolledOutBranch: Branch
): Rollout<Branch> {
if (rolloutInfo.defaultBranchId !== defaultBranch.id) {
throw new BranchMappingValidationError(
`Default branch id must match. Received: ${JSON.stringify(rolloutInfo)} ${defaultBranch.id}`
@@ -240,11 +252,22 @@ export function isRolloutBranchMapping(
return isUnconstrainedRollout(branchMapping) || isRtvConstrainedRollout(branchMapping);
}
export function isRollout(channelInfo: UpdateChannelBasicInfoFragment): boolean {
export function isRollout(channelInfo: ChannelBasicInfo): boolean {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
return isRolloutBranchMapping(branchMapping);
}
export function doesTargetRollout(
branchMapping: RolloutBranchMapping,
runtimeVersion: string
): boolean {
const rolloutInfo = getRolloutInfoFromBranchMapping(branchMapping);
if (!isConstrainedRolloutInfo(rolloutInfo)) {
return true;
}
return rolloutInfo.runtimeVersion === runtimeVersion;
}
export function createRolloutBranchMapping({
defaultBranchId,
rolloutBranchId,
@@ -302,7 +325,9 @@ function editRtvConstrainedRollout(
const nodesFromStatement = getNodesFromStatement(statementNode);
const rolloutNode = nodesFromStatement.find(isRolloutNode);
assert(rolloutNode, 'Rollout node must be defined.');
if (!rolloutNode) {
throw new BranchMappingValidationError('Rollout node must be defined.');
}
rolloutNode.operand = percent / 100;
return newBranchMapping;
}
+9 -2
View File
@@ -1,6 +1,10 @@
import chalk from 'chalk';
import { RuntimeFragment, UpdateFragment } from '../graphql/generated';
import {
RuntimeFragment,
UpdateBranchBasicInfoFragment,
UpdateFragment,
} from '../graphql/generated';
import { UpdateBranchObject, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import { promptAsync } from '../prompts';
@@ -13,7 +17,10 @@ export function printRollout(channel: UpdateChannelObject): void {
displayRolloutDetails(channel.name, rollout);
}
export function displayRolloutDetails(channelName: string, rollout: Rollout): void {
export function displayRolloutDetails(
channelName: string,
rollout: Rollout<UpdateBranchBasicInfoFragment>
): void {
const rolledOutPercent = rollout.percentRolledOut;
Log.newLine();
Log.log(chalk.bold('🚀 Rollout:'));
@@ -1,7 +1,7 @@
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { App, Runtime, UpdateBranch, UpdateChannel } from '../../graphql/generated';
import { App, Runtime, UpdateBranch, UpdateChannel, UpdateInsights } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { getBranchNameFromChannelNameAsync } from '../getBranchNameFromChannelNameAsync';
@@ -125,6 +125,7 @@ function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
createdAt: '2022-12-07T02:24:43.487Z',
runtimeVersion: '1.0.0',
runtime: {} as Runtime, // Temporary fix to resolve type errors
insights: {} as UpdateInsights, // Temporary fix to resolve type errors
platform: 'ios',
manifestFragment: '...',
isRollBackToEmbedded: false,
+43 -21
View File
@@ -5,14 +5,16 @@ import nullthrows from 'nullthrows';
import { getUpdateGroupUrl } from '../build/utils/url';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import fetch from '../fetch';
import { Update, UpdateInfoGroup } from '../graphql/generated';
import { UpdateFragment, UpdateInfoGroup } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import Log, { link } from '../log';
import { ora } from '../ora';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import {
CodeSigningInfo,
checkDirectiveBodyAgainstUpdateInfoGroup,
checkManifestBodyAgainstUpdateInfoGroup,
getDirectiveBodyAsync,
getManifestBodyAsync,
signBody,
} from '../utils/code-signing';
@@ -23,15 +25,7 @@ export type UpdateToRepublish = {
groupId: string;
branchId: string;
branchName: string;
} & Pick<
Update,
| 'message'
| 'runtimeVersion'
| 'manifestFragment'
| 'platform'
| 'gitCommitHash'
| 'codeSigningInfo'
>;
} & UpdateFragment;
/**
* @param updatesToPublish The update group to republish
@@ -65,6 +59,13 @@ export async function republishAsync({
update.branchName === arbitraryUpdate.branchName &&
update.runtimeVersion === arbitraryUpdate.runtimeVersion;
assert(updatesToPublish.every(isSameGroup), 'All updates must belong to the same update group');
assert(
updatesToPublish.every(u => u.isRollBackToEmbedded) ||
updatesToPublish.every(u => !u.isRollBackToEmbedded),
'All updates must either be roll back to embedded updates or not'
);
const { runtimeVersion } = arbitraryUpdate;
// If codesigning was created for the original update, we need to add it to the republish.
@@ -96,16 +97,25 @@ export async function republishAsync({
let updatesRepublished: Awaited<ReturnType<typeof PublishMutation.publishUpdateGroupAsync>>;
try {
const updateInfoGroup = Object.fromEntries(
updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])
);
const arbitraryUpdate = updatesToPublish[0];
const objectToMergeIn = arbitraryUpdate.isRollBackToEmbedded
? {
rollBackToEmbeddedInfoGroup: Object.fromEntries(
updatesToPublish.map(update => [update.platform, true])
),
}
: {
updateInfoGroup: Object.fromEntries(
updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])
),
};
updatesRepublished = await PublishMutation.publishUpdateGroupAsync(graphqlClient, [
{
branchId: targetBranchId,
runtimeVersion,
message: updateMessage,
updateInfoGroup,
...objectToMergeIn,
gitCommitHash: updatesToPublish[0].gitCommitHash,
awaitingCodeSigningInfo: !!codeSigningInfo,
},
@@ -120,19 +130,31 @@ export async function republishAsync({
method: 'GET',
headers: { accept: 'multipart/mixed' },
});
const manifestBody = nullthrows(await getManifestBodyAsync(response));
checkManifestBodyAgainstUpdateInfoGroup(
manifestBody,
nullthrows(nullthrows(updateInfoGroup)[newUpdate.platform as keyof UpdateInfoGroup])
);
let signature;
if (newUpdate.isRollBackToEmbedded) {
const directiveBody = nullthrows(await getDirectiveBodyAsync(response));
const manifestSignature = signBody(manifestBody, codeSigningInfo);
checkDirectiveBodyAgainstUpdateInfoGroup(directiveBody);
signature = signBody(directiveBody, codeSigningInfo);
} else {
const manifestBody = nullthrows(await getManifestBodyAsync(response));
checkManifestBodyAgainstUpdateInfoGroup(
manifestBody,
nullthrows(
nullthrows(objectToMergeIn.updateInfoGroup)[
newUpdate.platform as keyof UpdateInfoGroup
]
)
);
signature = signBody(manifestBody, codeSigningInfo);
}
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
keyid: codeSigningInfo.codeSigningMetadata.keyid,
sig: manifestSignature,
sig: signature,
});
})
);
@@ -1,5 +1,8 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonAccessor, EasJsonUtils, errors } from '@expo/eas-json';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import Log from '../../log';
import { selectAsync } from '../../prompts';
@@ -9,6 +12,7 @@ import {
maybePrintBuildProfileDeprecationWarningsAsync,
} from '../profiles';
jest.mock('fs');
jest.mock('../../prompts');
jest.mock('@expo/eas-json', () => {
const actual = jest.requireActual('@expo/eas-json');
@@ -32,6 +36,7 @@ const getBuildProfileDeprecationWarningsAsync = jest.spyOn(
);
const newLineSpy = jest.spyOn(Log, 'newLine');
const warnSpy = jest.spyOn(Log, 'warn');
const projectDir = '/app';
describe(getProfilesAsync, () => {
afterEach(() => {
@@ -51,6 +56,7 @@ describe(getProfilesAsync, () => {
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
type: 'build',
projectDir,
});
expect(result[0].profileName).toBe('production');
@@ -71,6 +77,7 @@ describe(getProfilesAsync, () => {
platforms: [Platform.ANDROID],
profileName: undefined,
type: 'build',
projectDir,
})
).rejects.toThrowError(errors.MissingProfileError);
});
@@ -86,6 +93,7 @@ describe(getProfilesAsync, () => {
platforms: [Platform.ANDROID, Platform.IOS],
profileName: 'custom-profile',
type: 'build',
projectDir,
});
expect(result[0].profileName).toBe('custom-profile');
@@ -98,6 +106,52 @@ describe(getProfilesAsync, () => {
expect(getBuildProfileAsync).toBeCalledWith(easJsonAccessor, Platform.IOS, 'custom-profile');
});
describe('node version', () => {
const nodeVersion = '14.17.1';
it('is read from profile', async () => {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
getBuildProfileAsync.mockImplementation(async () => {
return { node: nodeVersion } as BuildProfile<Platform.ANDROID>;
});
const result = await getProfilesAsync({
easJsonAccessor,
platforms: [Platform.ANDROID],
profileName: 'custom-profile',
type: 'build',
projectDir,
});
expect(result[0].profile.node).toBe(nodeVersion);
});
describe('with .nvmrc', () => {
beforeEach(async () => {
vol.reset();
await fs.mkdirp(os.tmpdir());
vol.fromJSON({ '.nvmrc': nodeVersion }, projectDir);
});
it('is read from .nvmrc', async () => {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
getBuildProfileAsync.mockImplementation(async () => {
return {} as BuildProfile<Platform.ANDROID>;
});
const result = await getProfilesAsync({
easJsonAccessor,
platforms: [Platform.ANDROID],
profileName: 'custom-profile',
type: 'build',
projectDir,
});
expect(result[0].profile.node).toBe(nodeVersion);
});
});
});
it('throws validation error if eas.json is invalid', async () => {
getBuildProfileAsync.mockImplementation(() => {
throw new errors.InvalidEasJsonError('eas.json is not valid');
@@ -109,6 +163,7 @@ describe(getProfilesAsync, () => {
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
type: 'build',
projectDir,
})
).rejects.toThrowError(/eas.json is not valid/);
});
+19
View File
@@ -56,7 +56,26 @@ export function shouldUseVersionedExpoCLIExpensive(
return !!resolveFrom.silent(projectDir, '@expo/cli');
}
/**
* Determine if we can and should use `expo export` with multiple `--platform` flags.
* This is an issue related to `expo export --all` causing issues when users have Metro web configured.
* See: https://github.com/expo/expo/pull/23621
*/
export function shouldUseVersionedExpoCLIWithExplicitPlatformsExpensive(
projectDir: string
): boolean {
const expoCliPath = resolveFrom.silent(projectDir, '@expo/cli/package.json');
if (!expoCliPath) {
return false;
}
return gteSdkVersion(require(expoCliPath).version, '0.10.11');
}
export const shouldUseVersionedExpoCLI = memoize(shouldUseVersionedExpoCLIExpensive);
export const shouldUseVersionedExpoCLIWithExplicitPlatforms = memoize(
shouldUseVersionedExpoCLIWithExplicitPlatformsExpensive
);
export async function expoCommandAsync(
projectDir: string,
+47 -2
View File
@@ -6,6 +6,9 @@ import {
ProfileType,
SubmitProfile,
} from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
import semver from 'semver';
import Log, { learnMore } from '../log';
@@ -24,19 +27,23 @@ export async function getProfilesAsync<T extends ProfileType>({
platforms,
profileName,
type,
projectDir,
}: {
easJsonAccessor: EasJsonAccessor;
platforms: Platform[];
profileName?: string;
projectDir: string;
type: T;
}): Promise<ProfileData<T>[]> {
const results = platforms.map(async function (platform) {
const profile = await readProfileAsync({
const profile = await readProfileWithOverridesAsync({
easJsonAccessor,
platform,
type,
profileName,
projectDir,
});
return {
profile,
profileName: profileName ?? 'production',
@@ -47,16 +54,53 @@ export async function getProfilesAsync<T extends ProfileType>({
return await Promise.all(results);
}
async function readProfileAsync<T extends ProfileType>({
async function maybeSetNodeVersionFromFileAsync(
projectDir: string,
profile: BuildProfile<Platform>
): Promise<void> {
if (profile?.node) {
return;
}
const nodeVersion = await getNodeVersionFromFileAsync(projectDir);
if (nodeVersion) {
Log.log(
`The EAS build profile does not specify a Node.js version. Using the version specified in .nvmrc: ${nodeVersion} `
);
profile.node = nodeVersion;
}
}
async function getNodeVersionFromFileAsync(projectDir: string): Promise<string | undefined> {
const nvmrcPath = path.join(projectDir, '.nvmrc');
if (!(await fs.pathExists(nvmrcPath))) {
return;
}
let nodeVersion: string;
try {
nodeVersion = (await fs.readFile(nvmrcPath, 'utf8')).toString().trim();
} catch {
return undefined;
}
if (!semver.valid(semver.coerce(nodeVersion))) {
throw new Error(`Invalid node version in .nvmrc: ${nodeVersion}`);
}
return nodeVersion;
}
async function readProfileWithOverridesAsync<T extends ProfileType>({
easJsonAccessor,
platform,
type,
profileName,
projectDir,
}: {
easJsonAccessor: EasJsonAccessor;
platform: Platform;
type: T;
profileName?: string;
projectDir: string;
}): Promise<EasProfile<T>> {
if (type === 'build') {
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
@@ -66,6 +110,7 @@ async function readProfileAsync<T extends ProfileType>({
);
await maybePrintBuildProfileDeprecationWarningsAsync(easJsonAccessor, platform, profileName);
await maybeSetNodeVersionFromFileAsync(projectDir, buildProfile);
return buildProfile as EasProfile<T>;
} else {
@@ -24,6 +24,8 @@ const humanReadableServiceName: Record<StatuspageServiceName, string> = {
[StatuspageServiceName.EasBuild]: 'EAS Build',
[StatuspageServiceName.EasSubmit]: 'EAS Submit',
[StatuspageServiceName.EasUpdate]: 'EAS Update',
[StatuspageServiceName.GithubApiRequests]: 'GitHub API Requests',
[StatuspageServiceName.GithubWebhooks]: 'Github Webhooks',
};
function warnAboutServiceOutage(service: StatuspageServiceFragment): void {
+3 -3
View File
@@ -1,19 +1,19 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "4.0.0",
"version": "5.2.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.18.6",
"@expo/eas-build-job": "1.0.30",
"@expo/eas-build-job": "1.0.39",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "10.1.0",
"golden-fleece": "1.0.9",
"joi": "17.7.0",
"log-symbols": "4.1.0",
"semver": "7.3.8",
"semver": "7.5.2",
"terminal-link": "2.1.1",
"tslib": "2.4.1"
},
+76 -14
View File
@@ -24,14 +24,19 @@
"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.",
@@ -91,12 +96,21 @@
"type": "object",
"description": "The build profile configuration.",
"properties": {
"withoutCredentials": {
"type": "boolean",
"description": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when working with custom builds.",
"markdownDescription": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when working with custom builds.",
"default": false
},
"extends": {
"type": "string",
"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",
@@ -118,7 +132,10 @@
"markdownDescription": "The channel is a name we can give to multiple builds to identify them easily. [Learn more](https://docs.expo.dev/eas-update/how-eas-update-works/)\n\n**This field only applies to the EAS Update service**, if your project still uses Classic Updates then use the [releaseChannel](https://docs.expo.dev/build-reference/eas-json/#releasechannel) field instead."
},
"distribution": {
"enum": ["internal", "store"],
"enum": [
"internal",
"store"
],
"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": [
@@ -147,6 +164,11 @@
"description": "The version of Yarn to use for the build. See: https://www.npmjs.com/package/yarn",
"markdownDescription": "The exact version of [Yarn](https://www.npmjs.com/package/yarn) to use for the build."
},
"pnpm": {
"type": "string",
"description": "The version of pnpm to use for the build. See: https://www.npmjs.com/package/pnpm",
"markdownDescription": "The exact version of [pnpm](https://www.npmjs.com/package/pnpm) to use for the build."
},
"expoCli": {
"type": "string",
"description": "The version of `expo-cli` used to `prebuild` your app.",
@@ -187,7 +209,11 @@
"default": "default",
"anyOf": [
{
"enum": ["default", "medium", "large"]
"enum": [
"default",
"medium",
"large"
]
},
{
"type": "string"
@@ -208,8 +234,8 @@
"properties": {
"withoutCredentials": {
"type": "boolean",
"description": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when you want to build debug binaries and the debug keystore is checked in to the repository.",
"markdownDescription": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when you want to build debug binaries and the debug keystore is checked in to the repository.",
"description": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when you want to build debug binaries and the debug keystore is checked in to the repository or working with custom builds.",
"markdownDescription": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when you want to build debug binaries and the debug keystore is checked in to the repository or working with custom builds.",
"default": false
},
"image": {
@@ -246,7 +272,11 @@
"default": "default",
"anyOf": [
{
"enum": ["default", "medium", "large"]
"enum": [
"default",
"medium",
"large"
]
},
{
"type": "string"
@@ -258,7 +288,12 @@
"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\"` - 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,
@@ -270,7 +305,10 @@
]
},
"buildType": {
"enum": ["app-bundle", "apk"],
"enum": [
"app-bundle",
"apk"
],
"description": "Type of the artifact you want to build.",
"markdownDescription": "Type of the artifact you want to build. It controls what Gradle task will be used, can be overridden by `gradleCommand` or `developmentClient: true` option.\n- `app-bundle` - `:app:bundleRelease`\n- `apk` - `:app:assembleRelease`",
"markdownEnumDescriptions": [
@@ -301,20 +339,34 @@
"type": "object",
"description": "The iOS-specific build profile configuration.",
"properties": {
"withoutCredentials": {
"type": "boolean",
"description": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when working with custom builds.",
"markdownDescription": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when working with custom builds.",
"default": false
},
"simulator": {
"type": "boolean",
"description": "If set to true, creates build for simulator.",
"default": false
},
"enterpriseProvisioning": {
"enum": ["universal", "adhoc"],
"enum": [
"universal",
"adhoc"
],
"markdownDescription": "Provisioning method used for `\"distribution\": \"internal\"` when you have an Apple account with Apple Developer Enterprise Program membership.\n\nYou can choose if you want to use `adhoc` or `universal` provisioning. The latter is recommended as it does not require you to register each individual device. If you don't provide this option and you still authenticate with an enterprise team, you'll be prompted which provisioning method to use.",
"markdownEnumDescriptions": [
"Recommended as it does not require you to register each individual device"
]
},
"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\"` - 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,
@@ -443,7 +495,12 @@
"markdownDescription": "Path to the JSON file with service account key used to authenticate with Google Play. [Learn more](https://expo.fyi/creating-google-service-account)"
},
"track": {
"enum": ["beta", "alpha", "internal", "production"],
"enum": [
"beta",
"alpha",
"internal",
"production"
],
"description": "The track of the application to use. Learn more: https://support.google.com/googleplay/android-developer/answer/9859348?hl=en",
"markdownDescription": "The [track of the application](https://support.google.com/googleplay/android-developer/answer/9859348?hl=en) to use.",
"markdownEnumDescriptions": [
@@ -454,7 +511,12 @@
]
},
"releaseStatus": {
"enum": ["draft", "inProgress", "halted", "completed"],
"enum": [
"draft",
"inProgress",
"halted",
"completed"
],
"description": "The status of a release. Learn more: https://developers.google.com/android-publisher/api-ref/rest/v3/edits.tracks",
"markdownDescription": "The status of a release. [Learn more](https://developers.google.com/android-publisher/api-ref/rest/v3/edits.tracks)",
"enumDescriptions": [
@@ -100,6 +100,105 @@ test('valid eas.json for development client builds', async () => {
});
});
test('valid eas.json with top level withoutCredentials property', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: {},
debug: {
developmentClient: true,
withoutCredentials: true,
},
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'debug');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'debug'
);
expect(androidProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: true,
});
expect(iosProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: true,
});
});
test('valid eas.json with iOS withoutCredentials property', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: {},
debug: {
developmentClient: true,
ios: { withoutCredentials: true },
},
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'debug');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'debug'
);
expect(androidProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
});
expect(iosProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: true,
});
});
test('valid eas.json with top level and platform-sepcific withoutCredentials property', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: {},
debug: {
developmentClient: true,
withoutCredentials: true,
ios: { withoutCredentials: false },
},
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'debug');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'debug'
);
expect(androidProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: true,
});
expect(iosProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: false,
});
});
test(`valid eas.json with autoIncrement flag at build profile root`, async () => {
await fs.writeJson('/project/eas.json', {
build: {
+8 -5
View File
@@ -25,9 +25,11 @@ const CacheSchema = Joi.object({
cacheDefaultPaths: Joi.boolean(),
customPaths: Joi.array().items(Joi.string()),
paths: Joi.array().items(Joi.string()),
}).rename('customPaths', 'paths')
})
.rename('customPaths', 'paths')
.messages({
'object.rename.override': 'Cannot provide both "cache.customPaths" and "cache.paths" - use "cache.paths"'
'object.rename.override':
'Cannot provide both "cache.customPaths" and "cache.paths" - use "cache.paths"',
});
const CommonBuildProfileSchema = Joi.object({
@@ -37,6 +39,7 @@ const CommonBuildProfileSchema = Joi.object({
// build environment
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
node: Joi.string().empty(null).custom(semverCheck),
pnpm: Joi.string().empty(null).custom(semverCheck),
yarn: Joi.string().empty(null).custom(semverCheck),
expoCli: Joi.string().empty(null).custom(semverCheck),
@@ -63,6 +66,9 @@ const CommonBuildProfileSchema = Joi.object({
// custom build configuration
config: Joi.string(),
// credentials
withoutCredentials: Joi.boolean(),
});
const PlatformBuildProfileSchema = CommonBuildProfileSchema.concat(
@@ -84,9 +90,6 @@ const AndroidBuildProfileSchema = PlatformBuildProfileSchema.concat(
// build environment
ndk: Joi.string().empty(null).custom(semverCheck),
// credentials
withoutCredentials: Joi.boolean(),
// build configuration
gradleCommand: Joi.string(),
buildType: Joi.string().valid('apk', 'app-bundle'),
+4 -3
View File
@@ -44,6 +44,7 @@ export interface CommonBuildProfile {
// build environment
env?: Record<string, string>;
node?: string;
pnpm?: string;
yarn?: string;
expoCli?: string;
@@ -70,6 +71,9 @@ export interface CommonBuildProfile {
// custom build configuration
config?: string;
// credentials
withoutCredentials?: boolean;
}
interface PlatformBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
@@ -86,9 +90,6 @@ export interface AndroidBuildProfile extends PlatformBuildProfile {
image?: Android.BuilderEnvironment['image'];
ndk?: string;
// credentials
withoutCredentials?: boolean;
// build configuration
gradleCommand?: string;
buildType?: Android.BuildType.APK | Android.BuildType.APP_BUNDLE;
+1 -1
View File
@@ -23,7 +23,7 @@
"fs-extra": "10.1.0",
"lodash-es": "4.17.21",
"marked": "3.0.4",
"semver": "7.3.8",
"semver": "7.5.2",
"tslib": "2.4.1"
},
"devDependencies": {
+4 -4
View File
@@ -166,10 +166,10 @@ [email protected]:
resolved "https://registry.yarnpkg.com/marked/-/marked-3.0.4.tgz#b8a1539e5e05c6ea9e93f15c0bad1d54ce890406"
integrity sha512-jBo8AOayNaEcvBhNobg6/BLhdsK3NvnKWJg33MAAPbvTWiG4QBn9gpW1+7RssrKu4K1dKlN+0goVQwV41xEfOA==
semver@7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
semver@7.5.2:
version "7.5.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb"
integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==
dependencies:
lru-cache "^6.0.0"
Generated
+22 -15
View File
@@ -1395,10 +1395,10 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/[email protected]0":
version "1.0.30"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.30.tgz#f8c57a673a2cc328f4041346996c480c3d910e92"
integrity sha512-KQGn9POn5p3guMNQJ5ykG3wffRlSEpjTCLO87d5lJll9E4u1grpqHJH2chOh5ICxg7tiWcO4iXt5S8tU7Ogpig==
"@expo/[email protected]9":
version "1.0.39"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.39.tgz#d3bc6cd50c499f07255bceeb9460e092a19505f7"
integrity sha512-OqCCxLx9HRMFQDiZvfpOfNmGhsTrV15IUOhmbp9iIa+uO/VyPpBvXqiA4ENCN9Jmf6yXtirIranCeJcm+jAuSA==
dependencies:
joi "^17.9.2"
semver "^7.5.4"
@@ -1576,10 +1576,10 @@
dependencies:
cross-spawn "^7.0.3"
"@expo/[email protected]1":
version "1.0.31"
resolved "https://registry.yarnpkg.com/@expo/steps/-/steps-1.0.31.tgz#61e0456e54bc63bd714aa40b9be27503c032caf0"
integrity sha512-TPuaihUQ9MfVmgG5OzFdMKXcBAgv1dfLZWGpk/k5hWBjPZAoe01UXbw8GCt6F09tS2Z256d2G/kOTJzISLNcjQ==
"@expo/[email protected]4":
version "1.0.34"
resolved "https://registry.yarnpkg.com/@expo/steps/-/steps-1.0.34.tgz#ba2f27a9d3e163b2d0fe6ef7775b237f7d18a286"
integrity sha512-jQoxeMWWl2mUwLGyiUD38z/FNao2jbDP8Tjvkr/YAfIOHaro36E3Vmn1YsKTYmtc41TGcFdCzqly95faRNrhvQ==
dependencies:
"@expo/logger" "1.0.30"
"@expo/spawn-async" "^1.7.2"
@@ -12084,10 +12084,10 @@ [email protected], semver@^7.3.2:
dependencies:
lru-cache "^6.0.0"
semver@7.3.8, semver@^7.0.0, semver@^7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
semver@7.5.2:
version "7.5.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb"
integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==
dependencies:
lru-cache "^6.0.0"
@@ -12110,6 +12110,13 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.0.0, semver@^7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
dependencies:
lru-cache "^6.0.0"
semver@^7.3.7:
version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
@@ -13651,9 +13658,9 @@ wonka@^6.3.2:
integrity sha512-2xXbQ1LnwNS7egVm1HPhW2FyKrekolzhpM3mCwXdQr55gO+tAiY76rhb32OL9kKsW8taj++iP7C6hxlVzbnvrw==
word-wrap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
version "1.2.5"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
wordwrap@^1.0.0:
version "1.0.0"