mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e4663ac44 | ||
|
|
ca18a555c1 | ||
|
|
c446bbb016 | ||
|
|
d5254434c6 | ||
|
|
e2277b9482 | ||
|
|
739502aef9 | ||
|
|
de42624992 | ||
|
|
0ccfbe5dd6 | ||
|
|
3ffad12681 | ||
|
|
565867b5a8 | ||
|
|
303cc82a06 | ||
|
|
4c2abc0693 | ||
|
|
b63167fbae | ||
|
|
8adb6434bf | ||
|
|
4c9e407fef | ||
|
|
ef566b3f9c | ||
|
|
895231ea98 | ||
|
|
c7000a754e | ||
|
|
9ac8404681 | ||
|
|
6c0b7d5951 | ||
|
|
ddfe60a7b0 | ||
|
|
7dedeb904b | ||
|
|
4c02e0102b | ||
|
|
e91516fcc7 | ||
|
|
d01f6c0d8e | ||
|
|
1ce69cd7c9 | ||
|
|
83c10793ac | ||
|
|
a344c11ba1 | ||
|
|
c7d5a9a819 | ||
|
|
2f43ed94ab | ||
|
|
4a4ebf3cd6 | ||
|
|
18b832dbda | ||
|
|
1200e7fecd | ||
|
|
3cc6b73d3a | ||
|
|
4831ff186a | ||
|
|
a2d60a4a76 | ||
|
|
9f604777b9 | ||
|
|
fce9bda9bb | ||
|
|
4cc6745a0d | ||
|
|
67f0b806f4 | ||
|
|
9280dfb3ad | ||
|
|
2befbb0eba | ||
|
|
41359b7194 | ||
|
|
c57861a67d | ||
|
|
d31ece8793 | ||
|
|
4f7148b756 | ||
|
|
bc8a781d86 | ||
|
|
153a70fb85 | ||
|
|
2f93492fc3 | ||
|
|
0b39ff1457 | ||
|
|
f6817037ae | ||
|
|
216f97ea8a | ||
|
|
11c194a168 | ||
|
|
3c74c111a7 | ||
|
|
b69b8b87a7 | ||
|
|
825baa9f68 | ||
|
|
9f5736cfaa | ||
|
|
ae24ffbe86 | ||
|
|
487316370b | ||
|
|
8bc3565270 | ||
|
|
07450f6bf7 | ||
|
|
71d48c3cf2 | ||
|
|
7343e81065 | ||
|
|
1f1310bef6 | ||
|
|
27c69ec8e1 | ||
|
|
b81b896bc6 | ||
|
|
ef79db2ca5 | ||
|
|
88565eec62 | ||
|
|
e8a520e55f | ||
|
|
9a33f7bd93 | ||
|
|
23c70e237b | ||
|
|
7fcf41db78 | ||
|
|
36f4a4bd38 | ||
|
|
87541db80d | ||
|
|
80112c682f | ||
|
|
eb29d3dadb |
No files matched your search
@@ -21,6 +21,13 @@ body:
|
||||
label: Summary
|
||||
description: Describe the issue in 1 or 2 sentences
|
||||
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Managed or bare?
|
||||
description: See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
|
||||
placeholder: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
+14
-11
@@ -1,15 +1,18 @@
|
||||
# Build directories
|
||||
packages/*/build
|
||||
packages/eas-cli/dist
|
||||
|
||||
# Code editors
|
||||
.idea
|
||||
.history
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
*-debug.log
|
||||
*-error.log
|
||||
|
||||
**/build/**
|
||||
!packages/eas-cli/src/commands/build/**
|
||||
!packages/eas-cli/src/build/**
|
||||
|
||||
dist
|
||||
/package-lock.json
|
||||
tmp
|
||||
node_modules
|
||||
# Tests
|
||||
coverage
|
||||
.history
|
||||
.DS_Store
|
||||
.idea
|
||||
+105
-1
@@ -6,6 +6,110 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates.
|
||||
- Adds `eas update`, which can bundle and publish updates on a branch.
|
||||
- Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels.
|
||||
- Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds.
|
||||
- Read more in our [feature preview docs](https://docs.expo.dev/eas-update/introduction/).
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas submit` displaying a prompt in non-interactive mode when some ASC API credentials are missing in `eas.json`. ([#841](https://github.com/expo/eas-cli/pull/841) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.41.0](https://github.com/expo/eas-cli/releases/tag/v0.41.0) - 2021-12-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Ask user to select profile if `release` does not exist. ([#829](https://github.com/expo/eas-cli/pull/829) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `build:inspect` command. ([#834](https://github.com/expo/eas-cli/pull/834) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix validating tool versions in eas.json. ([#832](https://github.com/expo/eas-cli/pull/832) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix submission URL. ([#845](https://github.com/expo/eas-cli/pull/845) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Warn when building managed project with SDK < 41. ([#833](https://github.com/expo/eas-cli/pull/833) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.40.0](https://github.com/expo/eas-cli/releases/tag/v0.40.0) - 2021-12-08
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- [eas-cli] change eas update:publish to eas update. ([#830](https://github.com/expo/eas-cli/pull/830) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Pass build profile environment to credentials command. ([#828](https://github.com/expo/eas-cli/pull/828) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.39.0](https://github.com/expo/eas-cli/releases/tag/v0.39.0) - 2021-12-06
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Set runtime version in `eas update:configure`. ([#811](https://github.com/expo/eas-cli/pull/811) by [@jkhales](https://github.com/jkhales))
|
||||
- Print project workflow in `eas diagnostics`. ([#822](https://github.com/expo/eas-cli/pull/822) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Improve error message for missing ascAppId. ([#813](https://github.com/expo/eas-cli/pull/813) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add `react-native` version to metadata. ([#823](https://github.com/expo/eas-cli/pull/823) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.38.3](https://github.com/expo/eas-cli/releases/tag/v0.38.3) - 2021-11-29
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Print warning about outdated version to stderr. ([#805](https://github.com/expo/eas-cli/pull/805) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix Apple sign in capabilities. ([#806](https://github.com/expo/eas-cli/pull/806) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.38.2](https://github.com/expo/eas-cli/releases/tag/v0.38.2) - 2021-11-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for extending submit profiles. ([#794](https://github.com/expo/eas-cli/pull/794) by [@dsokal](https://github.com/dsokal))
|
||||
- Add command: `eas update:configure`. ([#800](https://github.com/expo/eas-cli/pull/800) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix reading app version and app build version of iOS projects. ([#798](https://github.com/expo/eas-cli/pull/798) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix push notifications entitlements. ([#801](https://github.com/expo/eas-cli/pull/801) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.38.1](https://github.com/expo/eas-cli/releases/tag/v0.38.1) - 2021-11-25
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix building iOS projects with whitespace in the project name. ([#792](https://github.com/expo/eas-cli/pull/792) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.38.0](https://github.com/expo/eas-cli/releases/tag/v0.38.0) - 2021-11-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Compute runtime version policies. ([#783](https://github.com/expo/eas-cli/pull/783), [#785](https://github.com/expo/eas-cli/pull/785) by [@jkhales](https://github.com/jkhales))
|
||||
- Fix local builds with npm < 7. ([#787](https://github.com/expo/eas-cli/pull/787) by [@dsokal](https://github.com/dsokal))
|
||||
- Override `applicationId`/`bundleIdentifier` when auto-submitting after build. ([#780](https://github.com/expo/eas-cli/pull/780) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `typescript` to 4.5.2. Upgrade oclif dependencies. ([#781](https://github.com/expo/eas-cli/pull/781) by [@dsokal](https://github.com/dsokal))
|
||||
- Make missing `git` command error message more descriptive. ([#784](https://github.com/expo/eas-cli/pull/784) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.37.0](https://github.com/expo/eas-cli/releases/tag/v0.37.0) - 2021-11-18
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require explicitly defined applicationId/bundleIdentifier for EAS Submit in case of bare projects consisting of multiple product flavors on Android or multiple schemes/targets on iOS. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
@@ -68,7 +172,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🧹 Chores
|
||||
|
||||
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Bump `@expo/apple-utils` to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.37.0"
|
||||
"version": "0.41.1"
|
||||
}
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": ["packages/*"],
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
@@ -26,7 +28,7 @@
|
||||
"lerna": "4.0.0",
|
||||
"prettier": "2.4.1",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.4.2"
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.10.0",
|
||||
|
||||
+304
-43
@@ -7,9 +7,9 @@ EAS command line tool
|
||||
[](https://npmjs.org/package/eas-cli)
|
||||
[](https://github.com/expo/eas-cli/blob/main/package.json)
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Usage](#usage)
|
||||
* [Commands](#commands)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Commands](#commands)
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -37,11 +37,21 @@ eas --help COMMAND
|
||||
* [`eas account:logout`](#eas-accountlogout)
|
||||
* [`eas account:view`](#eas-accountview)
|
||||
* [`eas analytics [STATUS]`](#eas-analytics-status)
|
||||
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
|
||||
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
|
||||
* [`eas branch:list`](#eas-branchlist)
|
||||
* [`eas branch:rename`](#eas-branchrename)
|
||||
* [`eas branch:view [NAME]`](#eas-branchview-name)
|
||||
* [`eas build`](#eas-build)
|
||||
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:inspect`](#eas-buildinspect)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
|
||||
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
|
||||
* [`eas channel:list`](#eas-channellist)
|
||||
* [`eas channel:view [NAME]`](#eas-channelview-name)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
@@ -55,6 +65,10 @@ eas --help COMMAND
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit`](#eas-submit)
|
||||
* [`eas update`](#eas-update)
|
||||
* [`eas update:configure`](#eas-updateconfigure)
|
||||
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
|
||||
* [`eas update:view GROUPID`](#eas-updateview-groupid)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
* [`eas webhook:list`](#eas-webhooklist)
|
||||
@@ -73,7 +87,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -87,7 +101,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -101,7 +115,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -112,11 +126,92 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas branch:create [NAME]`
|
||||
|
||||
Create a branch on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:create [NAME]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the branch to create
|
||||
|
||||
OPTIONS
|
||||
--json return a json with the new branch ID and name.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
Delete a branch on the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:delete [NAME]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the branch to delete
|
||||
|
||||
OPTIONS
|
||||
--json return JSON with the edited branch's ID and name.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
List all branches on this project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:list
|
||||
|
||||
OPTIONS
|
||||
--json return output as JSON
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
Rename a branch.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:rename
|
||||
|
||||
OPTIONS
|
||||
--from=from current name of the branch.
|
||||
--json return a json with the edited branch's ID and name.
|
||||
--to=to new name of the branch.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
View a branch.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:view [NAME]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the branch to view
|
||||
|
||||
OPTIONS
|
||||
--json return a json with the branch's ID name and recent update groups.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
start a build
|
||||
Start a build
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -146,7 +241,7 @@ OPTIONS
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -157,7 +252,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -171,7 +266,39 @@ OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
Inspect the state of the project at specific build stages. Useful for troubleshooting.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:inspect
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios)
|
||||
(required)
|
||||
|
||||
-s, --stage=(archive|pre-build|post-build)
|
||||
(required) Stage of the build you want to inspect.
|
||||
archive - builds the project archive that would be uploaded to EAS when building
|
||||
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
|
||||
post-build - builds the native project and leaves the output directory for inspection
|
||||
|
||||
--force
|
||||
Delete OUTPUT_DIRECTORY if it already exists.
|
||||
|
||||
--output=OUTPUT_DIRECTORY
|
||||
(required) Output directory.
|
||||
|
||||
--profile=PROFILE_NAME
|
||||
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
|
||||
--verbose
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -204,7 +331,7 @@ OPTIONS
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -218,7 +345,73 @@ OPTIONS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
Create a channel on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:create [NAME]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the channel to create
|
||||
|
||||
OPTIONS
|
||||
--json print output as a JSON object with the new channel ID, name and branch mapping.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
Point a channel at a new branch.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:edit [NAME]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the channel to edit
|
||||
|
||||
OPTIONS
|
||||
--branch=branch Name of the branch to point to
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
List all channels on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:list
|
||||
|
||||
OPTIONS
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
View a channel on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:view [NAME]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the channel to view
|
||||
|
||||
OPTIONS
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -233,7 +426,7 @@ OPTIONS
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/config.js)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -244,7 +437,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -255,7 +448,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -269,7 +462,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -280,7 +473,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -291,7 +484,7 @@ USAGE
|
||||
$ eas diagnostics
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -308,7 +501,7 @@ OPTIONS
|
||||
--all see all commands in CLI
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.3/src/commands/help.ts)_
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.5/src/commands/help.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -319,7 +512,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -333,7 +526,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -350,7 +543,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -367,7 +560,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -378,11 +571,11 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
submit build archive to app store
|
||||
Submit build archive to App Store Connect
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -405,13 +598,81 @@ OPTIONS
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
See how to configure submits with eas.json:
|
||||
https://docs.expo.dev/submit/eas-json/
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/submit.js)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
Publish an update group.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all) [default: all]
|
||||
--auto Use the current git branch and commit message for the EAS branch and update message
|
||||
--branch=branch Branch to publish the update group on
|
||||
--group=group Update group to republish
|
||||
--input-dir=input-dir [default: dist] Location of the bundle
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--message=message A short message describing the update
|
||||
--republish Republish an update group
|
||||
--skip-bundler Skip running Expo CLI to bundle the app before publishing
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
Configure the project to support EAS Update.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
Delete all the updates in an update Group.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:delete GROUPID
|
||||
|
||||
ARGUMENTS
|
||||
GROUPID The ID of an update group to delete.
|
||||
|
||||
OPTIONS
|
||||
--json Return a json with the group ID of the deleted updates.
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
Update group details.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:view GROUPID
|
||||
|
||||
ARGUMENTS
|
||||
GROUPID The ID of an update group.
|
||||
|
||||
OPTIONS
|
||||
--json Return a json with the updates belonging to the group.
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -422,15 +683,15 @@ USAGE
|
||||
$ eas webhook:create
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
|
||||
header.
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the
|
||||
'Expo-Signature' header.
|
||||
|
||||
--url=url Webhook URL
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -444,7 +705,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -455,10 +716,10 @@ USAGE
|
||||
$ eas webhook:list
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -469,16 +730,16 @@ USAGE
|
||||
$ eas webhook:update
|
||||
|
||||
OPTIONS
|
||||
--event=(BUILD) [default: BUILD] Event type that triggers the webhook
|
||||
--id=id (required) Webhook ID
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
--id=id (required) Webhook ID
|
||||
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the 'Expo-Signature'
|
||||
header.
|
||||
--secret=secret Secret used to create a hash signature of the request payload, provided in the
|
||||
'Expo-Signature' header.
|
||||
|
||||
--url=url Webhook URL
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -492,5 +753,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.37.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.41.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+208
@@ -4807,6 +4807,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "reactNativeVersion",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "releaseChannel",
|
||||
"description": "",
|
||||
@@ -10674,6 +10686,12 @@
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "SUBMIT",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
@@ -12896,6 +12914,18 @@
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "couponCode",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
@@ -12985,6 +13015,34 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "discount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "InvoiceDiscount",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "totalDiscountedAmount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lineItems",
|
||||
"description": "",
|
||||
@@ -13079,6 +13137,132 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InvoiceDiscount",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "InvoiceDiscountType",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "duration",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "durationInMonths",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"description": "The coupon's discount value, in percentage or in dollar amount",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "InvoiceDiscountType",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "PERCENTAGE",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "AMOUNT",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InvoiceLineItem",
|
||||
@@ -14858,6 +15042,18 @@
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "couponCode",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
@@ -18823,6 +19019,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "reactNativeVersion",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "releaseChannel",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.37.0",
|
||||
"version": "0.41.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -9,24 +9,23 @@
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.26",
|
||||
"@expo/config": "6.0.3",
|
||||
"@expo/config-plugins": "4.0.3",
|
||||
"@expo/eas-build-job": "0.2.57",
|
||||
"@expo/eas-json": "^0.37.0",
|
||||
"@expo/json-file": "8.2.33",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.14",
|
||||
"@expo/plugin-warn-if-update-available": "1.7.0",
|
||||
"@expo/prebuild-config": "3.0.3",
|
||||
"@expo/config": "6.0.11",
|
||||
"@expo/config-plugins": "4.0.11",
|
||||
"@expo/eas-build-job": "0.2.61",
|
||||
"@expo/eas-json": "^0.41.0",
|
||||
"@expo/json-file": "8.2.34",
|
||||
"@expo/pkcs12": "0.0.6",
|
||||
"@expo/plist": "0.0.16",
|
||||
"@expo/plugin-warn-if-update-available": "1.7.1",
|
||||
"@expo/prebuild-config": "3.0.11",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.0",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/command": "1.8.0",
|
||||
"@oclif/config": "1.17.0",
|
||||
"@oclif/command": "1.8.3",
|
||||
"@oclif/config": "1.17.1",
|
||||
"@oclif/errors": "1.3.5",
|
||||
"@oclif/plugin-help": "3.2.3",
|
||||
"@oclif/plugin-help": "3.2.5",
|
||||
"@urql/core": "2.3.1",
|
||||
"@urql/exchange-retry": "0.3.0",
|
||||
"chalk": "4.1.2",
|
||||
@@ -96,7 +95,8 @@
|
||||
"mockdate": "^3.0.2",
|
||||
"nock": "^13.0.5",
|
||||
"tempy": "0.7.0",
|
||||
"ts-deepmerge": "1.0.8"
|
||||
"ts-deepmerge": "1.0.8",
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
|
||||
@@ -57,3 +57,13 @@ export function getExpoWebsiteBaseUrl(): string {
|
||||
return `https://expo.dev`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEASUpdateURL(projectId: string): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return new URL(projectId, `https://staging-u.expo.dev`).href;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return new URL(`expo-updates/${projectId}`, `http://127.0.0.1:3000`).href;
|
||||
} else {
|
||||
return new URL(projectId, `https://u.expo.dev`).href;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
JobData,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
@@ -28,9 +28,9 @@ import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
export async function createAndroidContextAsync(
|
||||
ctx: CommonContext<Platform.ANDROID>
|
||||
): Promise<AndroidBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
@@ -58,21 +58,26 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||
|
||||
return { applicationId, gradleContext };
|
||||
}
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
|
||||
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
|
||||
return await ensureAndroidCredentialsAsync(ctx);
|
||||
},
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
buildProfile: ctx.buildProfile,
|
||||
});
|
||||
},
|
||||
getMetadataContext: () => ({
|
||||
gradleContext,
|
||||
}),
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData<AndroidCredentials>
|
||||
@@ -100,8 +105,7 @@ function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
gradleContext?: GradleBuildContext
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
@@ -109,7 +113,7 @@ async function ensureAndroidCredentialsAsync(
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
gradleContext
|
||||
ctx.android.gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
app: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json';
|
||||
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -41,7 +41,7 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: AndroidBuildProfile;
|
||||
buildProfile: BuildProfile<Platform.ANDROID>;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
const { autoIncrement } = buildProfile;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function prepareJobAsync(
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
|
||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const { credentials } = jobData;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { sleepAsync } from '../utils/promise';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { MetadataContext, collectMetadataAsync } from './metadata';
|
||||
import { collectMetadataAsync } from './metadata';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
@@ -39,7 +39,6 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
getMetadataContext: () => MetadataContext<TPlatform>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
@@ -78,7 +77,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
);
|
||||
}
|
||||
|
||||
const projectArchive = ctx.local
|
||||
const projectArchive = ctx.localBuildOptions.enable
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
@@ -88,16 +87,15 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
const metadataContext = builder.getMetadataContext();
|
||||
const metadata = await collectMetadataAsync(ctx, metadataContext);
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (ctx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
if (ctx.localBuildOptions.enable) {
|
||||
await runLocalBuildAsync(job, ctx.localBuildOptions);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
|
||||
@@ -58,7 +58,6 @@ export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -144,7 +143,7 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
|
||||
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
const reader = new EasJsonReader(ctx.projectDir);
|
||||
await reader.readAndValidateAsync();
|
||||
await reader.readAsync();
|
||||
|
||||
Log.withTick('Validated eas.json');
|
||||
return;
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { XcodeBuildContext } from '../project/ios/scheme';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export interface ConfigureContext {
|
||||
user: Actor;
|
||||
@@ -27,6 +23,21 @@ export interface ConfigureContext {
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||
|
||||
export interface AndroidBuildContext {
|
||||
applicationId: string;
|
||||
gradleContext?: GradleBuildContext;
|
||||
}
|
||||
|
||||
export interface IosBuildContext {
|
||||
bundleIdentifier: string;
|
||||
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
|
||||
applicationTarget: Target;
|
||||
targets: Target[];
|
||||
xcodeBuildContext: XcodeBuildContext;
|
||||
}
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
accountName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -34,7 +45,7 @@ export interface BuildContext<T extends Platform> {
|
||||
clearCache: boolean;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp: ExpoConfig;
|
||||
local: boolean;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
@@ -44,112 +55,6 @@ export interface BuildContext<T extends Platform> {
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
}
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
local,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: accountName,
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
return {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
local,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
localBuildOptions,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user,
|
||||
env: buildProfile.env,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
localBuildOptions,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
if (platform === Platform.ANDROID) {
|
||||
const common = commonContext as CommonContext<Platform.ANDROID>;
|
||||
return {
|
||||
...common,
|
||||
android: await createAndroidContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
} else {
|
||||
const common = commonContext as CommonContext<Platform.IOS>;
|
||||
return {
|
||||
...common,
|
||||
ios: await createIosContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
}
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,17 @@ import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../pro
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { IosMetadataContext } from '../metadata';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
|
||||
interface BuildConfiguration {
|
||||
targetName?: string;
|
||||
buildConfiguration?: string;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
export async function createIosContextAsync(
|
||||
ctx: CommonContext<Platform.IOS>
|
||||
): Promise<IosBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
@@ -50,29 +43,42 @@ export async function prepareIosBuildAsync(
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
|
||||
|
||||
return {
|
||||
bundleIdentifier: applicationTarget.bundleIdentifier,
|
||||
applicationTarget,
|
||||
applicationTargetBuildSettings,
|
||||
targets,
|
||||
xcodeBuildContext,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, targets);
|
||||
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
},
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile,
|
||||
buildSettings: buildConfiguration.buildSettings,
|
||||
buildProfile: ctx.buildProfile,
|
||||
buildSettings: ctx.ios.applicationTargetBuildSettings,
|
||||
});
|
||||
},
|
||||
getMetadataContext: (): IosMetadataContext => {
|
||||
return buildConfiguration;
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
|
||||
return await prepareJobAsync(ctx, {
|
||||
...jobData,
|
||||
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
||||
});
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
@@ -90,23 +96,17 @@ export async function prepareIosBuildAsync(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveBuildConfiguration(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): BuildConfiguration {
|
||||
function resolveBuildSettings(
|
||||
ctx: CommonContext<Platform.IOS>,
|
||||
applicationTarget: Target
|
||||
): XCBuildConfiguration['buildSettings'] {
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
return { buildSettings: {} };
|
||||
return {};
|
||||
}
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
});
|
||||
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
|
||||
return {
|
||||
buildSettings,
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
};
|
||||
return xcBuildConfiguration?.buildSettings ?? {};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
@@ -36,7 +36,7 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: IosBuildProfile;
|
||||
buildProfile: BuildProfile<Platform.IOS>;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
|
||||
@@ -130,6 +130,7 @@ export async function maybeResolveVersionsAsync(
|
||||
};
|
||||
} catch (err: any) {
|
||||
Log.warn('Failed to read app versions.');
|
||||
Log.debug(err);
|
||||
Log.warn(err.message);
|
||||
Log.warn('Proceeding anyway...');
|
||||
return {};
|
||||
@@ -200,7 +201,12 @@ function ensureStaticConfigExists(projectDir: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
|
||||
export function evaluateTemplateString(
|
||||
s: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): string {
|
||||
// necessary because XCBuildConfiguration['buildSettings'] is not a plain object
|
||||
const vars = { ...buildSettings };
|
||||
return s.replace(/\$\((\w+)\)/g, (match, key) => {
|
||||
if (vars.hasOwnProperty(key)) {
|
||||
const value = String(vars[key]);
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.50';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.56';
|
||||
|
||||
export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
const { command, args } = getCommandAndArgs(job);
|
||||
await spawnAsync(command, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
skipCleanup?: boolean;
|
||||
skipNativeBuild?: boolean;
|
||||
artifactsDir?: string;
|
||||
workingdir?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
function getCommandAndArgs(job: Job): { command: string; args: string[] } {
|
||||
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
|
||||
const { command, args } = await getCommandAndArgsAsync(job);
|
||||
let spinner;
|
||||
if (!options.verbose) {
|
||||
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
|
||||
}
|
||||
let childProcess: ChildProcess | undefined;
|
||||
const interruptHandler = (): void => {
|
||||
if (childProcess) {
|
||||
childProcess.kill();
|
||||
}
|
||||
};
|
||||
process.on('SIGINT', interruptHandler);
|
||||
try {
|
||||
const spawnPromise = spawnAsync(command, args, {
|
||||
stdio: options.verbose ? 'inherit' : 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
|
||||
...(options.skipCleanup || options.skipNativeBuild
|
||||
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
|
||||
: {}),
|
||||
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
|
||||
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
|
||||
},
|
||||
});
|
||||
childProcess = spawnPromise.child;
|
||||
await spawnPromise;
|
||||
} finally {
|
||||
process.removeListener('SIGINT', interruptHandler);
|
||||
spinner?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
|
||||
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
|
||||
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
|
||||
return {
|
||||
@@ -19,9 +59,20 @@ function getCommandAndArgs(job: Job): { command: string; args: string[] } {
|
||||
args: [jobBase64],
|
||||
};
|
||||
} else {
|
||||
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
|
||||
if (await isAtLeastNpm7Async()) {
|
||||
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
|
||||
// prompting the user to install a package that is used for the first time
|
||||
args.unshift('-y');
|
||||
}
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', `${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64],
|
||||
args,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function isAtLeastNpm7Async(): Promise<boolean> {
|
||||
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
|
||||
return semver.gte(version, '7.0.0');
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
import fs from 'fs-extra';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { getApplicationIdAsync } from '../project/android/applicationId';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import Log from '../log';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
@@ -22,22 +22,20 @@ import {
|
||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidMetadataContext
|
||||
: IosMetadataContext;
|
||||
|
||||
export interface AndroidMetadataContext {
|
||||
gradleContext?: GradleBuildContext;
|
||||
}
|
||||
export interface IosMetadataContext {
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
targetName?: string;
|
||||
buildConfiguration?: string;
|
||||
// TODO(JJ): Replace this with the getRuntimeVersionNullable function in @expo/config-plugins
|
||||
function getRuntimeVersionNullable(
|
||||
...[config, platform]: Parameters<typeof Updates.getRuntimeVersion>
|
||||
): string | null {
|
||||
try {
|
||||
return Updates.getRuntimeVersion(config, platform);
|
||||
} catch (e) {
|
||||
Log.debug(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
ctx: BuildContext<T>
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const distribution =
|
||||
@@ -46,16 +44,17 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
...(await maybeResolveVersionsAsync(ctx, platformContext)),
|
||||
...(await maybeResolveVersionsAsync(ctx)),
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: ctx.exp.runtimeVersion,
|
||||
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx),
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await getVcsClient().getCommitHashAsync(),
|
||||
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
|
||||
@@ -70,15 +69,14 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
}
|
||||
|
||||
async function maybeResolveVersionsAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = platformContext as IosMetadataContext;
|
||||
const iosContext = ctx as BuildContext<Platform.IOS>;
|
||||
return await maybeResolveIosVersionsAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
iosContext?.buildSettings ?? {}
|
||||
iosContext.ios.applicationTargetBuildSettings
|
||||
);
|
||||
} else if (ctx.platform === Platform.ANDROID) {
|
||||
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
|
||||
@@ -88,19 +86,11 @@ async function maybeResolveVersionsAsync<T extends Platform>(
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAppIdentifierAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
): Promise<string> {
|
||||
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = platformContext as IosMetadataContext;
|
||||
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp, {
|
||||
targetName: iosContext.targetName,
|
||||
buildConfiguration: iosContext.buildConfiguration,
|
||||
});
|
||||
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
|
||||
} else {
|
||||
const androidContext = platformContext as AndroidMetadataContext;
|
||||
return await getApplicationIdAsync(ctx.projectDir, ctx.exp, androidContext.gradleContext);
|
||||
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +144,19 @@ async function getNativeChannelAsync<T extends Platform>(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function getReactNativeVersionAsync(
|
||||
ctx: BuildContext<Platform>
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
|
||||
return (await fs.readJson(reactNativePackageJsonPath)).version;
|
||||
} catch (err) {
|
||||
Log.debug('Failed to resolve react-native version:');
|
||||
Log.debug(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIosEnterpriseProvisioning(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): IosEnterpriseProvisioning | undefined {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
|
||||
import { createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from './build';
|
||||
import { ensureProjectConfiguredAsync } from './configure';
|
||||
import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
|
||||
let metroConfigValidated = false;
|
||||
let sdkVersionChecked = false;
|
||||
|
||||
export interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
skipProjectConfiguration: boolean;
|
||||
profile?: string;
|
||||
nonInteractive: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir, flags.requestedPlatform);
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile ?? undefined,
|
||||
});
|
||||
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
buildProfiles,
|
||||
});
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildFragment;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
}
|
||||
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
||||
Log.newLine();
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.submitProfile,
|
||||
type: 'submit',
|
||||
});
|
||||
for (const startedBuild of startedBuilds) {
|
||||
const submitProfile = nullthrows(
|
||||
submitProfiles.find(
|
||||
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
|
||||
)
|
||||
).profile;
|
||||
const submission = await prepareAndStartSubmissionAsync({
|
||||
build: startedBuild.build,
|
||||
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
buildProfile: startedBuild.buildProfile.profile,
|
||||
submitProfile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
submissions.push(submission);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
return;
|
||||
}
|
||||
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
|
||||
printBuildResults(builds, flags.json);
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||
);
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
} else {
|
||||
// the following function also exits with non zero code if any of the submissions failed
|
||||
await waitForSubmissionsToCompleteAsync(submissions);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: buildProfile.profile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform: buildProfile.platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
localBuildOptions: flags.localBuildOptions,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(buildProfile.platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} build`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (buildCtx.workflow === Workflow.MANAGED) {
|
||||
if (!sdkVersionChecked) {
|
||||
await checkExpoSdkIsSupportedAsync(buildCtx);
|
||||
sdkVersionChecked = true;
|
||||
}
|
||||
if (!metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
|
||||
metroConfigValidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const build = await startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
buildCtx,
|
||||
};
|
||||
}
|
||||
|
||||
async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
let sendBuildRequestAsync: BuildRequestSender;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
|
||||
} else {
|
||||
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
|
||||
}
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
async function prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
buildCtx,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
submitProfile,
|
||||
nonInteractive,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
buildCtx: BuildContext<Platform>;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
submitProfile: SubmitProfile;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const platform = toPlatform(build.platform);
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId: build.project.id,
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive,
|
||||
env: buildProfile.env,
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[build.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return await submitAsync(submissionCtx);
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import path from 'path';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { CommonContext } from './context';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
|
||||
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||
Log.warn(
|
||||
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
@@ -16,7 +16,7 @@ export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
|
||||
}
|
||||
|
||||
export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
ctx: CommonContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
|
||||
return;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
import Log from '../log';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
@@ -27,8 +26,6 @@ export default abstract class EasCommand extends Command {
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
EasJsonReader.setLog(Log);
|
||||
|
||||
await initAnalyticsAsync();
|
||||
await this.applyCliConfigAsync();
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ export async function createUpdateBranchOnAppAsync({
|
||||
}
|
||||
|
||||
export default class BranchCreate extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Create a branch on the current project.';
|
||||
|
||||
static args = [
|
||||
|
||||
@@ -76,8 +76,7 @@ async function deleteBranchOnAppAsync({
|
||||
}
|
||||
|
||||
export default class BranchDelete extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Republish an update group';
|
||||
static description = 'Delete a branch on the current project';
|
||||
|
||||
static args = [
|
||||
{
|
||||
|
||||
@@ -54,8 +54,6 @@ export async function listBranchesAsync({
|
||||
}
|
||||
|
||||
export default class BranchList extends EasCommand {
|
||||
static hidden = true;
|
||||
|
||||
static description = 'List all branches on this project.';
|
||||
|
||||
static flags = {
|
||||
@@ -76,7 +74,7 @@ export default class BranchList extends EasCommand {
|
||||
Log.log(JSON.stringify(branches, null, 2));
|
||||
} else {
|
||||
const table = new CliTable({
|
||||
head: ['branch', ...UPDATE_COLUMNS],
|
||||
head: ['Branch', ...UPDATE_COLUMNS],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,475 +1,10 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import dateFormat from 'dateformat';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetUpdateGroupAsyncQuery,
|
||||
Robot,
|
||||
RootQueryUpdatesByGroupArgs,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
User,
|
||||
ViewBranchQuery,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
import { createUpdateBranchOnAppAsync } from './create';
|
||||
import { listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
type PlatformFlag = PublishPlatform | 'all';
|
||||
|
||||
async function getUpdateGroupAsync({
|
||||
group,
|
||||
}: RootQueryUpdatesByGroupArgs): Promise<GetUpdateGroupAsyncQuery['updatesByGroup']> {
|
||||
const { updatesByGroup } = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetUpdateGroupAsyncQuery, RootQueryUpdatesByGroupArgs>(
|
||||
gql`
|
||||
query getUpdateGroupAsync($group: ID!) {
|
||||
updatesByGroup(group: $group) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
manifestFragment
|
||||
platform
|
||||
message
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
group,
|
||||
},
|
||||
{ additionalTypenames: ['Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return updatesByGroup;
|
||||
}
|
||||
|
||||
async function ensureChannelExistsAsync({
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await createUpdateChannelOnAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
Log.withTick(
|
||||
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
|
||||
);
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureBranchExistsAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
}: {
|
||||
appId: string;
|
||||
name: string;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
updates: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'];
|
||||
}> {
|
||||
const { app } = await viewUpdateBranchAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
const updateBranch = app?.byId.updateBranchByName;
|
||||
if (updateBranch) {
|
||||
const { id, updates } = updateBranch;
|
||||
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
|
||||
return { id, updates };
|
||||
}
|
||||
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
|
||||
return { id: newUpdateBranch.id, updates: [] };
|
||||
}
|
||||
|
||||
export default class BranchPublish extends EasCommand {
|
||||
static description = 'deprecated, use "eas update"';
|
||||
static hidden = true;
|
||||
static description = 'Publish an update group to a branch.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Name of the branch to publish on',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
message: flags.string({
|
||||
description: 'short message describing the updates.',
|
||||
required: false,
|
||||
}),
|
||||
republish: flags.boolean({
|
||||
description: 'republish an update group',
|
||||
exclusive: ['input-dir', 'skip-bundler'],
|
||||
}),
|
||||
group: flags.string({
|
||||
description: 'update group to republish',
|
||||
exclusive: ['input-dir', 'skip-bundler'],
|
||||
}),
|
||||
'input-dir': flags.string({
|
||||
description: 'location of the bundle',
|
||||
default: 'dist',
|
||||
required: false,
|
||||
}),
|
||||
'skip-bundler': flags.boolean({
|
||||
description: `skip running Expo CLI to bundle the app before publishing`,
|
||||
default: false,
|
||||
}),
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
description: `only publish to a single platform`,
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: `return a json with the new update group.`,
|
||||
default: false,
|
||||
}),
|
||||
auto: flags.boolean({
|
||||
description:
|
||||
'use the current git branch and commit message for the EAS branch and update message',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
args: { name: branchName },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
auto: autoFlag,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
},
|
||||
} = this.parse(BranchPublish);
|
||||
const platformFlag = this.parse(BranchPublish).flags.platform as PlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!branchName && autoFlag) {
|
||||
branchName =
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`;
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
branchName = await selectAsync<string>(
|
||||
'which branch would you like to publish on?',
|
||||
branches.map(branch => {
|
||||
return {
|
||||
title: `${branch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdate(branch.updates[0])}`
|
||||
)}`,
|
||||
value: branch.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
assert(branchName, 'branch name must be specified.');
|
||||
}
|
||||
|
||||
const { id: branchId, updates } = await ensureBranchExistsAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let oldMessage: string, oldRuntimeVersion: string;
|
||||
if (republish) {
|
||||
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
|
||||
// Instead we get the `updateInfoGroup` from the update we wish to republish.
|
||||
let updatesToRepublish: Pick<
|
||||
Update,
|
||||
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
|
||||
>[];
|
||||
if (group) {
|
||||
updatesToRepublish = await getUpdateGroupAsync({ group });
|
||||
} else {
|
||||
// Drop into interactive mode if the user has not specified an update group to republish.
|
||||
if (jsonFlag) {
|
||||
throw new Error('You must specify the update group to republish.');
|
||||
}
|
||||
|
||||
const updateGroups = uniqBy(updates, u => u.group)
|
||||
.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return platformFlag === 'all' || update.platform === platformFlag;
|
||||
})
|
||||
.map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
|
||||
// Only republish to the specified platforms
|
||||
update => platformFlag === 'all' || update.platform === platformFlag
|
||||
);
|
||||
if (updatesToRepublishFilteredByPlatform.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
let publicationPlatformMessage: string;
|
||||
if (platformFlag === 'all') {
|
||||
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
|
||||
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
|
||||
}
|
||||
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
|
||||
.map(update => update.platform)
|
||||
.join(',')}`;
|
||||
} else {
|
||||
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
|
||||
}
|
||||
Log.withTick(publicationPlatformMessage);
|
||||
|
||||
for (const update of updatesToRepublishFilteredByPlatform) {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
} else {
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
await buildBundlesAsync({ projectDir, inputDir });
|
||||
}
|
||||
|
||||
const assetSpinner = ora('Uploading assets...').start();
|
||||
try {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!message && autoFlag) {
|
||||
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Please enter a publication message.`,
|
||||
initial: republish
|
||||
? `Republish "${oldMessage!}" - group: ${group}`
|
||||
: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const runtimeToPlatformMapping: Record<string, string[]> = {};
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
|
||||
.filter(pair => pair[1] === runtime)
|
||||
.map(pair => pair[0]);
|
||||
}
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
|
||||
])
|
||||
);
|
||||
|
||||
if (republish && !oldRuntimeVersion) {
|
||||
throw new Error(
|
||||
'Can not find the runtime version of the update group that is being republished.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion : runtime,
|
||||
message,
|
||||
};
|
||||
});
|
||||
let newUpdates;
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to published updates');
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(newUpdates));
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
const platforms = newUpdates
|
||||
.filter(update => update.runtimeVersion === runtime)
|
||||
.map(update => update.platform);
|
||||
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
|
||||
if (!newUpdate) {
|
||||
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
|
||||
}
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'branch', value: branchName },
|
||||
{ label: 'runtime version', value: runtime },
|
||||
{ label: 'platform', value: platforms.join(',') },
|
||||
{ label: 'update group ID', value: newUpdate.group },
|
||||
{ label: 'message', value: message },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
throw new Error(BranchPublish.description);
|
||||
}
|
||||
}
|
||||
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: PlatformFlag,
|
||||
projectDir: string
|
||||
): Promise<Record<string, string>> {
|
||||
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
|
||||
|
||||
for (const platform of platforms) {
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error('Runtime version policies are only supported in the managed workflow.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
|
||||
);
|
||||
}
|
||||
|
||||
function formatUpdateTitle(
|
||||
update: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'][number]
|
||||
): string {
|
||||
const { message, createdAt, actor, runtimeVersion } = update;
|
||||
|
||||
let actorName: string;
|
||||
switch (actor?.__typename) {
|
||||
case 'User': {
|
||||
actorName = (actor as Pick<User, 'username' | 'id'>).username;
|
||||
break;
|
||||
}
|
||||
case 'Robot': {
|
||||
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
|
||||
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
actorName = 'unknown';
|
||||
}
|
||||
return `[${dateFormat(
|
||||
createdAt,
|
||||
'mmm dd HH:MM'
|
||||
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
|
||||
}
|
||||
@@ -51,7 +51,6 @@ async function renameUpdateBranchOnAppAsync({
|
||||
}
|
||||
|
||||
export default class BranchRename extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Rename a branch.';
|
||||
|
||||
static flags = {
|
||||
|
||||
@@ -67,7 +67,6 @@ export async function viewUpdateBranchAsync({
|
||||
}
|
||||
|
||||
export default class BranchView extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'View a branch.';
|
||||
|
||||
static args = [
|
||||
|
||||
@@ -7,6 +7,7 @@ import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export default class BuildConfigure extends EasCommand {
|
||||
static description = 'Configure the project to support EAS Build.';
|
||||
@@ -21,11 +22,14 @@ export default class BuildConfigure extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = this.parse(BuildConfigure);
|
||||
|
||||
Log.log(
|
||||
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
|
||||
const platform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
|
||||
@@ -48,7 +52,7 @@ function logSuccess(platform: RequestedPlatform): void {
|
||||
storesText = 'the Google Play Store';
|
||||
} else if (platform === 'ios') {
|
||||
platformsText = 'iOS project is';
|
||||
storesText = 'the Apple App Store';
|
||||
storesText = 'the Apple App Store Connect';
|
||||
}
|
||||
|
||||
Log.log(`🎉 Your ${platformsText} ready to build.
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { prepareAndroidBuildAsync } from '../../build/android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import { BuildContext, createBuildContextAsync } from '../../build/context';
|
||||
import { prepareIosBuildAsync } from '../../build/ios/build';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
SubmissionFragment,
|
||||
} from '../../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../../platform';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { createSubmissionContextAsync } from '../../submit/context';
|
||||
import {
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../../submit/utils/urls';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../../utils/profiles';
|
||||
import { getVcsClient, setVcsClient } from '../../vcs';
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
|
||||
@@ -61,21 +31,8 @@ interface RawBuildFlags {
|
||||
'auto-submit-with-profile'?: string;
|
||||
}
|
||||
|
||||
interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
skipProjectConfiguration: boolean;
|
||||
profile: string | null;
|
||||
nonInteractive: boolean;
|
||||
local: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
static description = 'start a build';
|
||||
static description = 'Start a build';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({
|
||||
@@ -129,102 +86,17 @@ export default class Build extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
private metroConfigValidated = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = this.parse(Build);
|
||||
if (rawFlags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
const { requestedPlatform } = flags;
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
buildProfiles,
|
||||
});
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildFragment;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const { build: maybeBuild, buildCtx } = await this.prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
}
|
||||
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||
}
|
||||
|
||||
if (flags.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
||||
Log.newLine();
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
for (const startedBuild of startedBuilds) {
|
||||
const submission = await this.prepareAndStartSubmissionAsync({
|
||||
build: startedBuild.build,
|
||||
credentialsCtx: nullthrows(
|
||||
buildCtxByPlatform[startedBuild.build.platform]?.credentialsCtx
|
||||
),
|
||||
flags,
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
buildProfile: startedBuild.buildProfile,
|
||||
});
|
||||
submissions.push(submission);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
return;
|
||||
}
|
||||
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
|
||||
printBuildResults(builds, flags.json);
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||
);
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
} else {
|
||||
// the following function also exits with non zero code if any of the submissions failed
|
||||
await waitForSubmissionsToCompleteAsync(submissions);
|
||||
}
|
||||
await runBuildAndSubmitAsync(projectDir, flags);
|
||||
}
|
||||
|
||||
private async sanitizeFlagsAsync(flags: RawBuildFlags): Promise<BuildFlags> {
|
||||
@@ -262,9 +134,13 @@ export default class Build extends EasCommand {
|
||||
return {
|
||||
requestedPlatform,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
profile: profile ?? null,
|
||||
profile,
|
||||
nonInteractive,
|
||||
local: flags['local'],
|
||||
localBuildOptions: flags['local']
|
||||
? { enable: true, verbose: true }
|
||||
: {
|
||||
enable: false,
|
||||
},
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
json: flags['json'],
|
||||
@@ -272,110 +148,6 @@ export default class Build extends EasCommand {
|
||||
submitProfile: flags['auto-submit-with-profile'] ?? profile,
|
||||
};
|
||||
}
|
||||
|
||||
private async prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: buildProfile.profile,
|
||||
local: flags.local,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform: buildProfile.platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(buildProfile.platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} build`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (buildCtx.workflow === Workflow.MANAGED && !this.metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
|
||||
this.metroConfigValidated = true;
|
||||
}
|
||||
|
||||
const build = await this.startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
buildCtx,
|
||||
};
|
||||
}
|
||||
|
||||
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
let sendBuildRequestAsync: BuildRequestSender;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
|
||||
} else {
|
||||
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
|
||||
}
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
private async prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
credentialsCtx,
|
||||
flags,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
credentialsCtx: CredentialsContext;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platform = toPlatform(build.platform);
|
||||
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId: build.project.id,
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive: flags.nonInteractive,
|
||||
env: buildProfile.profile.env,
|
||||
credentialsCtx,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[build.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return await submitAsync(submissionCtx);
|
||||
}
|
||||
|
||||
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDeprecatedEasJsonAsync(
|
||||
@@ -387,7 +159,7 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
return;
|
||||
}
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const rawEasJson = await easJsonReader.readRawAsync();
|
||||
const rawEasJson = await easJsonReader.readAsync();
|
||||
if (rawEasJson?.cli) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { getTmpDirectory } from '../../utils/paths';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
enum InspectStage {
|
||||
ARCHIVE = 'archive',
|
||||
PRE_BUILD = 'pre-build',
|
||||
POST_BUILD = 'post-build',
|
||||
}
|
||||
|
||||
const STAGE_DESCRIPTION = `Stage of the build you want to inspect.
|
||||
archive - builds the project archive that would be uploaded to EAS when building
|
||||
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
|
||||
post-build - builds the native project and leaves the output directory for inspection`;
|
||||
|
||||
export default class BuildInspect extends EasCommand {
|
||||
static description =
|
||||
'Inspect the state of the project at specific build stages. Useful for troubleshooting.';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
options: [RequestedPlatform.Android, RequestedPlatform.Ios],
|
||||
required: true,
|
||||
}),
|
||||
profile: flags.string({
|
||||
description:
|
||||
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
stage: flags.enum({
|
||||
char: 's',
|
||||
description: STAGE_DESCRIPTION,
|
||||
options: [InspectStage.ARCHIVE, InspectStage.PRE_BUILD, InspectStage.POST_BUILD],
|
||||
required: true,
|
||||
}),
|
||||
output: flags.string({
|
||||
description: 'Output directory.',
|
||||
required: true,
|
||||
helpValue: 'OUTPUT_DIRECTORY',
|
||||
}),
|
||||
force: flags.boolean({
|
||||
description: 'Delete OUTPUT_DIRECTORY if it already exists.',
|
||||
default: false,
|
||||
}),
|
||||
verbose: flags.boolean({
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = this.parse(BuildInspect);
|
||||
const outputDirectory = path.resolve(process.cwd(), flags.output);
|
||||
await this.prepareOutputDirAsync(outputDirectory, flags.force);
|
||||
if (flags.stage === InspectStage.ARCHIVE) {
|
||||
const vcs = getVcsClient();
|
||||
await vcs.ensureRepoExistsAsync();
|
||||
await vcs.makeShallowCopyAsync(outputDirectory);
|
||||
Log.withTick(`Project saved to ${outputDirectory}`);
|
||||
} else {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
|
||||
try {
|
||||
await runBuildAndSubmitAsync(projectDir, {
|
||||
skipProjectConfiguration: false,
|
||||
nonInteractive: false,
|
||||
wait: true,
|
||||
clearCache: false,
|
||||
json: false,
|
||||
autoSubmit: false,
|
||||
requestedPlatform: flags.platform,
|
||||
profile: flags.profile,
|
||||
localBuildOptions: {
|
||||
enable: true,
|
||||
...(flags.stage === InspectStage.PRE_BUILD ? { skipNativeBuild: true } : {}),
|
||||
...(flags.stage === InspectStage.POST_BUILD ? { skipCleanup: true } : {}),
|
||||
verbose: flags.verbose,
|
||||
workingdir: tmpWorkingdir,
|
||||
artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
|
||||
},
|
||||
});
|
||||
if (!flags.verbose) {
|
||||
Log.log(chalk.green('Build successful'));
|
||||
}
|
||||
} catch (err) {
|
||||
if (!flags.verbose) {
|
||||
Log.error('Build failed');
|
||||
Log.error(`Re-run this command with ${chalk.bold('--verbose')} flag to see the logs`);
|
||||
}
|
||||
} finally {
|
||||
const spinner = ora().start(`Copying project build directory to ${outputDirectory}`);
|
||||
try {
|
||||
const tmpBuildDirectory = path.join(tmpWorkingdir, 'build');
|
||||
if (await fs.pathExists(tmpBuildDirectory)) {
|
||||
await fs.copy(tmpBuildDirectory, outputDirectory);
|
||||
}
|
||||
await fs.remove(tmpWorkingdir);
|
||||
spinner.succeed(`Project build directory saved to ${outputDirectory}`);
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async prepareOutputDirAsync(outputDir: string, force: boolean): Promise<void> {
|
||||
if (await fs.pathExists(outputDir)) {
|
||||
if (force) {
|
||||
await fs.remove(outputDir);
|
||||
} else {
|
||||
throw new Error(`Directory ${outputDir} already exists`);
|
||||
}
|
||||
}
|
||||
await fs.mkdirp(outputDir);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,6 @@ export async function createUpdateChannelOnAppAsync({
|
||||
}
|
||||
|
||||
export default class ChannelCreate extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Create a channel on the current project.';
|
||||
|
||||
static args = [
|
||||
|
||||
@@ -89,13 +89,12 @@ export async function updateChannelBranchMappingAsync({
|
||||
);
|
||||
const channel = data.updateChannel.editUpdateChannel;
|
||||
if (!channel) {
|
||||
throw new Error(`Could not fine channel with id ${channelId}`);
|
||||
throw new Error(`Could not find a channel with id: ${channelId}`);
|
||||
}
|
||||
return data.updateChannel.editUpdateChannel!;
|
||||
}
|
||||
|
||||
export default class ChannelEdit extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Point a channel at a new branch.';
|
||||
|
||||
static args = [
|
||||
|
||||
@@ -66,7 +66,6 @@ async function getAllUpdateChannelForAppAsync({
|
||||
}
|
||||
|
||||
export default class ChannelList extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'List all channels on the current project.';
|
||||
|
||||
static flags = {
|
||||
|
||||
@@ -181,7 +181,7 @@ export function logChannelDetails(channel: {
|
||||
: []),
|
||||
formatUpdate(update),
|
||||
update?.runtimeVersion ?? 'N/A',
|
||||
update?.group ?? 'N/A,',
|
||||
update?.group ?? 'N/A',
|
||||
getPlatformsForGroup({
|
||||
updates: branch.updates,
|
||||
group: branch.updates[0]?.group,
|
||||
@@ -192,7 +192,6 @@ export function logChannelDetails(channel: {
|
||||
}
|
||||
|
||||
export default class ChannelView extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'View a channel on the current project.';
|
||||
|
||||
static args = [
|
||||
@@ -239,7 +238,7 @@ export default class ChannelView extends EasCommand {
|
||||
});
|
||||
const channel = getUpdateChannelByNameForAppresult.app?.byId.updateChannelByName;
|
||||
if (!channel) {
|
||||
throw new Error(`Could not fine channel with name ${channelName}`);
|
||||
throw new Error(`Could not find a channel with name: ${channelName}`);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
@@ -256,9 +255,7 @@ export default class ChannelView extends EasCommand {
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk`{bold Branches, pointed at by this channel, and their most recent update group:}`
|
||||
);
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
logChannelDetails(channel);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export default class Config extends EasCommand {
|
||||
},
|
||||
]));
|
||||
|
||||
const profile = await reader.readBuildProfileAsync(platform, profileName);
|
||||
const profile = await reader.getBuildProfileAsync(platform, profileName);
|
||||
const config = getExpoConfig(projectDir, { env: profile.env, isPublicConfig: true });
|
||||
|
||||
Log.log(getProjectConfigDescription(projectDir));
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { SelectPlatform } from '../credentials/manager/SelectPlatform';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
export default class Credentials extends EasCommand {
|
||||
static description = 'manage your credentials';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
user: await ensureLoggedInAsync(),
|
||||
});
|
||||
await new SelectPlatform().runAsync(ctx);
|
||||
await new SelectPlatform().runAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import envinfo from 'envinfo';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import Log from '../log';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
export default class Diagnostics extends EasCommand {
|
||||
@@ -25,6 +28,7 @@ export default class Diagnostics extends EasCommand {
|
||||
'react-navigation',
|
||||
'@expo/webpack-config',
|
||||
'expo-dev-client',
|
||||
'expo-updates',
|
||||
],
|
||||
npmGlobalPackages: ['eas-cli', 'expo-cli'],
|
||||
},
|
||||
@@ -32,6 +36,23 @@ export default class Diagnostics extends EasCommand {
|
||||
title: `EAS CLI ${easCliVersion} environment info`,
|
||||
}
|
||||
);
|
||||
Log.log(info);
|
||||
|
||||
Log.log(info.trimEnd());
|
||||
await this.printWorkflowAsync();
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
private async printWorkflowAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
|
||||
if (androidWorkflow === iosWorkflow) {
|
||||
Log.log(` Project workflow: ${androidWorkflow}`);
|
||||
} else {
|
||||
Log.log(` Project Workflow:`);
|
||||
Log.log(` Android: ${androidWorkflow}`);
|
||||
Log.log(` iOS: ${iosWorkflow}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ interface Flags {
|
||||
}
|
||||
|
||||
export default class Submit extends EasCommand {
|
||||
static description = `submit build archive to app store
|
||||
static description = `Submit build archive to App Store Connect
|
||||
See how to configure submits with eas.json: ${link('https://docs.expo.dev/submit/eas-json/')}`;
|
||||
static aliases = ['build:submit'];
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ExpoConfig, getConfig, modifyConfigAsync } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
|
||||
const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
|
||||
const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0';
|
||||
|
||||
async function configureProjectForEASUpdateAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
isBare: boolean
|
||||
): Promise<void> {
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const easUpdateURL = getEASUpdateURL(projectId);
|
||||
const preexistingRuntimeVersion = exp.runtimeVersion;
|
||||
const defaultRuntimeVersion = isBare
|
||||
? DEFAULT_BARE_RUNTIME_VERSION
|
||||
: DEFAULT_MANAGED_RUNTIME_VERSION;
|
||||
const result = await modifyConfigAsync(projectDir, {
|
||||
runtimeVersion: preexistingRuntimeVersion ?? defaultRuntimeVersion,
|
||||
updates: { ...exp.updates, url: easUpdateURL },
|
||||
});
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
if (exp.updates?.url) {
|
||||
if (exp.updates.url !== easUpdateURL) {
|
||||
Log.withTick(
|
||||
`Overwrote "${exp.updates?.url}" with "${easUpdateURL}" for the updates.url value in app.json`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Log.withTick(`Set updates.url value, to "${easUpdateURL}" in app.json`);
|
||||
}
|
||||
if (!preexistingRuntimeVersion) {
|
||||
Log.withTick(
|
||||
`Set runtimeVersion to "${JSON.stringify(defaultRuntimeVersion)}" in app.json`
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'warn': {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`It looks like you are using a dynamic configuration! ${learnMore(
|
||||
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs)'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:\n${learnMore(
|
||||
'https://expo.fyi/eas-update-config.md'
|
||||
)}\n`
|
||||
);
|
||||
Log.log(
|
||||
chalk.bold(
|
||||
`{\n updates": {\n "url": "${easUpdateURL}"\n },\n "runtimeVersion": {\n "policy": "sdkVersion"\n }\n}`
|
||||
)
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
case 'fail':
|
||||
throw new Error(result.message);
|
||||
default:
|
||||
throw new Error('Unexpected result type from modifyConfigAsync');
|
||||
}
|
||||
}
|
||||
|
||||
export default class UpdateConfigure extends EasCommand {
|
||||
static description = 'Configure the project to support EAS Update.';
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
|
||||
);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
});
|
||||
|
||||
const hasAndroidNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||
const hasIosNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||
const isBare = hasAndroidNativeProject || hasIosNativeProject;
|
||||
|
||||
await configureProjectForEASUpdateAsync(projectDir, exp, isBare);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
if (isBare) {
|
||||
Log.log(
|
||||
`🧐 It seems you are on the bare workflow! Please also update your native files. You can do this by either running ${chalk.bold(
|
||||
'eas build:configure'
|
||||
)} or manually editing Expo.plist/AndroidManifest.xml. ${learnMore(
|
||||
'https://expo.fyi/eas-update-config.md#native-configuration'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
Log.log(`🎉 Your app is configured to run EAS Update!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ async function deleteUpdateGroupAsync({
|
||||
}
|
||||
|
||||
export default class UpdateDelete extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Delete all the updates in an update Group.';
|
||||
|
||||
static args = [
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import dateFormat from 'dateformat';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetUpdateGroupAsyncQuery,
|
||||
Robot,
|
||||
RootQueryUpdatesByGroupArgs,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
User,
|
||||
ViewBranchQuery,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
PublishPlatform,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { createUpdateBranchOnAppAsync } from '../branch/create';
|
||||
import { listBranchesAsync } from '../branch/list';
|
||||
import { viewUpdateBranchAsync } from '../branch/view';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
type PlatformFlag = PublishPlatform | 'all';
|
||||
|
||||
async function getUpdateGroupAsync({
|
||||
group,
|
||||
}: RootQueryUpdatesByGroupArgs): Promise<GetUpdateGroupAsyncQuery['updatesByGroup']> {
|
||||
const { updatesByGroup } = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetUpdateGroupAsyncQuery, RootQueryUpdatesByGroupArgs>(
|
||||
gql`
|
||||
query getUpdateGroupAsync($group: ID!) {
|
||||
updatesByGroup(group: $group) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
manifestFragment
|
||||
platform
|
||||
message
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
group,
|
||||
},
|
||||
{ additionalTypenames: ['Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return updatesByGroup;
|
||||
}
|
||||
|
||||
async function ensureChannelExistsAsync({
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await createUpdateChannelOnAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
Log.withTick(
|
||||
`Created a channel: ${chalk.bold(channelName)} pointed at branch: ${chalk.bold(channelName)}.`
|
||||
);
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureBranchExistsAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
}: {
|
||||
appId: string;
|
||||
name: string;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
updates: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'];
|
||||
}> {
|
||||
const { app } = await viewUpdateBranchAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
const updateBranch = app?.byId.updateBranchByName;
|
||||
if (updateBranch) {
|
||||
const { id, updates } = updateBranch;
|
||||
await ensureChannelExistsAsync({ appId, branchId: id, channelName: branchName });
|
||||
return { id, updates };
|
||||
}
|
||||
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync({ appId, name: branchName });
|
||||
Log.withTick(`Created branch: ${chalk.bold(branchName)}`);
|
||||
await ensureChannelExistsAsync({ appId, branchId: newUpdateBranch.id, channelName: branchName });
|
||||
return { id: newUpdateBranch.id, updates: [] };
|
||||
}
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
static description = 'Publish an update group.';
|
||||
|
||||
static flags = {
|
||||
branch: flags.string({
|
||||
description: 'Branch to publish the update group on',
|
||||
required: false,
|
||||
}),
|
||||
message: flags.string({
|
||||
description: 'A short message describing the update',
|
||||
required: false,
|
||||
}),
|
||||
republish: flags.boolean({
|
||||
description: 'Republish an update group',
|
||||
exclusive: ['input-dir', 'skip-bundler'],
|
||||
}),
|
||||
group: flags.string({
|
||||
description: 'Update group to republish',
|
||||
exclusive: ['input-dir', 'skip-bundler'],
|
||||
}),
|
||||
'input-dir': flags.string({
|
||||
description: 'Location of the bundle',
|
||||
default: 'dist',
|
||||
required: false,
|
||||
}),
|
||||
'skip-bundler': flags.boolean({
|
||||
description: `Skip running Expo CLI to bundle the app before publishing`,
|
||||
default: false,
|
||||
}),
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr',
|
||||
default: false,
|
||||
}),
|
||||
auto: flags.boolean({
|
||||
description:
|
||||
'Use the current git branch and commit message for the EAS branch and update message',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
let {
|
||||
flags: {
|
||||
branch: branchName,
|
||||
json: jsonFlag,
|
||||
auto: autoFlag,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
},
|
||||
} = this.parse(UpdatePublish);
|
||||
const platformFlag = this.parse(UpdatePublish).flags.platform as PlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, {
|
||||
skipSDKVersionRequirement: true,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
await checkEASUpdateURLIsSetAsync(exp);
|
||||
|
||||
if (!branchName && autoFlag) {
|
||||
branchName =
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`;
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
const validationMessage = 'Branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const branches = await listBranchesAsync({ projectId });
|
||||
if (branches.length === 0) {
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Creating a new one. Please name the new branch:',
|
||||
initial:
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substr(2, 4)}`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
} else {
|
||||
branchName = await selectAsync<string>(
|
||||
'Which branch would you like to publish on?',
|
||||
branches.map(branch => {
|
||||
return {
|
||||
title: `${branch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdate(branch.updates[0])}`
|
||||
)}`,
|
||||
value: branch.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
}
|
||||
|
||||
const { id: branchId, updates } = await ensureBranchExistsAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let oldMessage: string, oldRuntimeVersion: string;
|
||||
if (republish) {
|
||||
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
|
||||
// Instead we get the `updateInfoGroup` from the update we wish to republish.
|
||||
let updatesToRepublish: Pick<
|
||||
Update,
|
||||
'group' | 'message' | 'runtimeVersion' | 'manifestFragment' | 'platform'
|
||||
>[];
|
||||
if (group) {
|
||||
updatesToRepublish = await getUpdateGroupAsync({ group });
|
||||
} else {
|
||||
// Drop into interactive mode if the user has not specified an update group to republish.
|
||||
if (jsonFlag) {
|
||||
throw new Error('You must specify the update group to republish.');
|
||||
}
|
||||
|
||||
const updateGroups = uniqBy(updates, u => u.group)
|
||||
.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return platformFlag === 'all' || update.platform === platformFlag;
|
||||
})
|
||||
.map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
|
||||
// Only republish to the specified platforms
|
||||
update => platformFlag === 'all' || update.platform === platformFlag
|
||||
);
|
||||
if (updatesToRepublishFilteredByPlatform.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
let publicationPlatformMessage: string;
|
||||
if (platformFlag === 'all') {
|
||||
if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) {
|
||||
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
|
||||
}
|
||||
publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform
|
||||
.map(update => update.platform)
|
||||
.join(', ')}`;
|
||||
} else {
|
||||
publicationPlatformMessage = `The republished update will appear only on: ${platformFlag}`;
|
||||
}
|
||||
Log.withTick(publicationPlatformMessage);
|
||||
|
||||
for (const update of updatesToRepublishFilteredByPlatform) {
|
||||
const { manifestFragment } = update;
|
||||
const platform = update.platform as PublishPlatform;
|
||||
|
||||
unsortedUpdateInfoGroups[platform] = JSON.parse(manifestFragment);
|
||||
}
|
||||
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublishFilteredByPlatform[0].group;
|
||||
oldMessage = updatesToRepublishFilteredByPlatform[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublishFilteredByPlatform[0].runtimeVersion;
|
||||
|
||||
if (!message) {
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Please enter an update message.`,
|
||||
initial: `Republish "${oldMessage!}" - group: ${group}`,
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
if (!message && autoFlag) {
|
||||
message = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ publishMessage: message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Please enter an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
await buildBundlesAsync({ projectDir, inputDir });
|
||||
}
|
||||
|
||||
const assetSpinner = ora('Uploading assets...').start();
|
||||
try {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeToPlatformMapping: Record<string, string[]> = {};
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
|
||||
.filter(pair => pair[1] === runtime)
|
||||
.map(pair => pair[0]);
|
||||
}
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups = Object.entries(runtimeToPlatformMapping).map(([runtime, platforms]) => {
|
||||
const localUpdateInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
unsortedUpdateInfoGroups[platform as keyof UpdateInfoGroup],
|
||||
])
|
||||
);
|
||||
|
||||
if (republish && !oldRuntimeVersion) {
|
||||
throw new Error(
|
||||
'Can not find the runtime version of the update group that is being republished.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion : runtime,
|
||||
message,
|
||||
};
|
||||
});
|
||||
let newUpdates;
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdates = await PublishMutation.publishUpdateGroupAsync(updateGroups);
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to published updates');
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(newUpdates));
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
const platforms = newUpdates
|
||||
.filter(update => update.runtimeVersion === runtime)
|
||||
.map(update => update.platform);
|
||||
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
|
||||
if (!newUpdate) {
|
||||
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
|
||||
}
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'branch', value: branchName },
|
||||
{ label: 'runtime version', value: runtime },
|
||||
{ label: 'platform', value: platforms.join(', ') },
|
||||
{ label: 'update group ID', value: newUpdate.group },
|
||||
{ label: 'message', value: message! },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: PlatformFlag,
|
||||
projectDir: string
|
||||
): Promise<Record<string, string>> {
|
||||
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
|
||||
|
||||
for (const platform of platforms) {
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged = (await resolveWorkflowAsync(projectDir, platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error('Runtime version policies are only supported in the managed workflow.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
platforms.map(platform => [platform, Updates.getRuntimeVersion(exp, platform)])
|
||||
);
|
||||
}
|
||||
|
||||
function formatUpdateTitle(
|
||||
update: Exclude<
|
||||
Exclude<ViewBranchQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'][number]
|
||||
): string {
|
||||
const { message, createdAt, actor, runtimeVersion } = update;
|
||||
|
||||
let actorName: string;
|
||||
switch (actor?.__typename) {
|
||||
case 'User': {
|
||||
actorName = (actor as Pick<User, 'username' | 'id'>).username;
|
||||
break;
|
||||
}
|
||||
case 'Robot': {
|
||||
const { firstName, id } = actor as Pick<Robot, 'firstName' | 'id'>;
|
||||
actorName = firstName ?? `robot: ${id.slice(0, 4)}...`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
actorName = 'unknown';
|
||||
}
|
||||
return `[${dateFormat(
|
||||
createdAt,
|
||||
'mmm dd HH:MM'
|
||||
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
|
||||
}
|
||||
|
||||
async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
|
||||
const configuredURL = exp.updates?.url;
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const expectedURL = getEASUpdateURL(projectId);
|
||||
|
||||
if (configuredURL !== expectedURL) {
|
||||
throw new Error(
|
||||
`The update URL is incorrectly configured for EAS Update. Please set updates.url to ${expectedURL} in your app.json.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,6 @@ export async function viewUpdateAsync({
|
||||
return data;
|
||||
}
|
||||
export default class UpdateView extends EasCommand {
|
||||
static hidden = true;
|
||||
static description = 'Update group details.';
|
||||
|
||||
static args = [
|
||||
|
||||
@@ -14,8 +14,7 @@ export default class WebhookCreate extends EasCommand {
|
||||
static flags = {
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
default: WebhookType.Build,
|
||||
options: [WebhookType.Build, WebhookType.Submit],
|
||||
}),
|
||||
url: flags.string({
|
||||
description: 'Webhook URL',
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class WebhookList extends EasCommand {
|
||||
static flags = {
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
options: [WebhookType.Build, WebhookType.Submit],
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ export default class WebhookUpdate extends EasCommand {
|
||||
}),
|
||||
event: flags.enum({
|
||||
description: 'Event type that triggers the webhook',
|
||||
options: [WebhookType.Build],
|
||||
default: WebhookType.Build,
|
||||
options: [WebhookType.Build, WebhookType.Submit],
|
||||
}),
|
||||
url: flags.string({
|
||||
description: 'Webhook URL',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../log';
|
||||
@@ -27,6 +28,7 @@ export class CredentialsContext {
|
||||
nonInteractive?: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
this.projectDir = options.projectDir;
|
||||
@@ -35,11 +37,21 @@ export class CredentialsContext {
|
||||
|
||||
this.resolvedExp = options.exp;
|
||||
if (!this.resolvedExp) {
|
||||
try {
|
||||
this.resolvedExp = getExpoConfig(options.projectDir);
|
||||
} catch (error) {
|
||||
// ignore error, context might be created outside of expo project
|
||||
}
|
||||
this.resolvedExp =
|
||||
CredentialsContext.getExpoConfigInProject(this.projectDir, { env: options.env }) ??
|
||||
undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static getExpoConfigInProject(
|
||||
projectDir: string,
|
||||
{ env }: { env?: Env } = {}
|
||||
): ExpoConfig | null {
|
||||
try {
|
||||
return getExpoConfig(projectDir, { env });
|
||||
} catch (error) {
|
||||
// ignore error, context might be created outside of expo project
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,9 @@ async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<Minimal
|
||||
|
||||
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
|
||||
Log.log(
|
||||
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
|
||||
chalk.bold(
|
||||
'An App Store Connect Api key is required to upload your app to the Apple App Store Connect'
|
||||
)
|
||||
);
|
||||
Log.log(
|
||||
`If you're not sure what this is or how to create one, ${learnMore(
|
||||
|
||||
@@ -150,6 +150,30 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
expect(results.disabled).toStrictEqual([]);
|
||||
});
|
||||
|
||||
// It's unclear what is happening in this issue, so for now we can provide a more
|
||||
// helpful error message until we can get a reproducible example.
|
||||
// https://github.com/expo/eas-cli/issues/740
|
||||
it('throw useful error message on unexpected error', async () => {
|
||||
const bundleId = {
|
||||
getBundleIdCapabilitiesAsync: jest.fn(() => []),
|
||||
updateBundleIdCapabilityAsync: jest.fn(() => {
|
||||
throw new Error(`bundle 'XXX333' cannot be deleted. Delete all the Apps`);
|
||||
}),
|
||||
attributes: {
|
||||
identifier: 'dev.expo.app',
|
||||
},
|
||||
id: 'XXX333',
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
syncCapabilitiesForEntitlementsAsync(bundleId, {
|
||||
'com.apple.developer.healthkit': true,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`https://developer.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot skip complex duplicates', async () => {
|
||||
const bundleId = {
|
||||
getBundleIdCapabilitiesAsync: jest.fn(() => [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@expo/apple-utils';
|
||||
import { JSONObject, JSONValue } from '@expo/json-file';
|
||||
import getenv from 'getenv';
|
||||
import { inspect } from 'util';
|
||||
|
||||
import Log from '../../../log';
|
||||
|
||||
@@ -100,7 +101,20 @@ export async function syncCapabilitiesForEntitlementsAsync(
|
||||
);
|
||||
|
||||
if (modifiedRequest.length) {
|
||||
await bundleId.updateBundleIdCapabilityAsync(modifiedRequest);
|
||||
Log.debug(`Patch Request:`, inspect(modifiedRequest, { depth: null, colors: true }));
|
||||
try {
|
||||
await bundleId.updateBundleIdCapabilityAsync(modifiedRequest);
|
||||
} catch (error: any) {
|
||||
if (error.message.match(/bundle '[\w\d]+' cannot be deleted. Delete all the Apps/)) {
|
||||
Log.error(
|
||||
'Failed to patch capabilities:',
|
||||
inspect(modifiedRequest, { depth: null, colors: true })
|
||||
);
|
||||
throw new Error(
|
||||
`Unexpected error occurred while attempting to update capabilities for app "${bundleId.attributes.identifier}".\nCapabilities can be modified manually in the Apple developer console at https://developer.apple.com/account/resources/identifiers/bundleId/edit/${bundleId.id}.\nAuto capability syncing can be disabled with the environment variable \`EXPO_NO_CAPABILITY_SYNC=1\`.\n${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { enabled: enabledCapabilityNames, disabled: disabledCapabilityNames };
|
||||
|
||||
@@ -162,11 +162,13 @@ export async function ensureAppExistsAsync(
|
||||
}
|
||||
): Promise<App> {
|
||||
const context = getRequestContext(authCtx);
|
||||
const spinner = ora(`Linking to App Store ${chalk.dim(bundleIdentifier)}`).start();
|
||||
const spinner = ora(`Linking to App Store Connect ${chalk.dim(bundleIdentifier)}`).start();
|
||||
|
||||
let app = await App.findAsync(context, { bundleId: bundleIdentifier });
|
||||
if (!app) {
|
||||
spinner.text = `Creating App Store app ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`;
|
||||
spinner.text = `Creating App Store Connect app ${chalk.bold(name)} ${chalk.dim(
|
||||
bundleIdentifier
|
||||
)}`;
|
||||
try {
|
||||
// Assert contract errors when the user needs to create an app.
|
||||
await assertContractMessagesAsync(context, spinner);
|
||||
@@ -194,6 +196,8 @@ export async function ensureAppExistsAsync(
|
||||
} else {
|
||||
// TODO: Update app name when API gives us that possibility
|
||||
}
|
||||
spinner.succeed(`Prepared App Store for ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`);
|
||||
spinner.succeed(
|
||||
`Prepared App Store Connect for ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`
|
||||
);
|
||||
return app;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IOSConfig, compileModsAsync } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import { getPrebuildConfig } from '@expo/prebuild-config';
|
||||
import { getPrebuildConfigAsync } from '@expo/prebuild-config';
|
||||
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getManagedEntitlementsJsonAsync(
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
const { exp } = getPrebuildConfig(projectDir, { platforms: ['ios'] });
|
||||
const { exp } = await getPrebuildConfigAsync(projectDir, { platforms: ['ios'] });
|
||||
|
||||
const expWithMods = await compileModsAsync(exp, {
|
||||
projectRoot: projectDir,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
|
||||
import Log, { learnMore } from '../../log';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { ensureActorHasUsername } from '../../user/actions';
|
||||
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
|
||||
import { AssignFcm } from '../android/actions/AssignFcm';
|
||||
import { AssignGoogleServiceAccountKey } from '../android/actions/AssignGoogleServiceAccountKey';
|
||||
import {
|
||||
@@ -51,12 +51,18 @@ import {
|
||||
import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
|
||||
|
||||
export class ManageAndroid {
|
||||
constructor(private callingAction: Action) {}
|
||||
constructor(private callingAction: Action, private projectDir: string) {}
|
||||
|
||||
async runAsync(
|
||||
ctx: CredentialsContext,
|
||||
currentActions: ActionInfo[] = highLevelActions
|
||||
): Promise<void> {
|
||||
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
|
||||
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir);
|
||||
const buildProfile = hasProjectContext
|
||||
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
|
||||
: null;
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
user: await ensureLoggedInAsync(),
|
||||
env: buildProfile?.env,
|
||||
});
|
||||
const accountName = ctx.hasProjectContext
|
||||
? getProjectAccountName(ctx.exp, ctx.user)
|
||||
: ensureActorHasUsername(ctx.user);
|
||||
@@ -64,7 +70,12 @@ export class ManageAndroid {
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
const { gradleContext } = await this.createProjectContextAsync(ctx);
|
||||
|
||||
let gradleContext;
|
||||
if (ctx.hasProjectContext) {
|
||||
assert(buildProfile, 'buildProfile must be defined in a project context');
|
||||
gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@@ -140,30 +151,18 @@ export class ManageAndroid {
|
||||
}
|
||||
}
|
||||
|
||||
private async createProjectContextAsync(ctx: CredentialsContext): Promise<{
|
||||
gradleContext?: GradleBuildContext;
|
||||
buildProfile?: AndroidBuildProfile;
|
||||
}> {
|
||||
if (!ctx.hasProjectContext) {
|
||||
return {};
|
||||
}
|
||||
|
||||
private async createProjectContextAsync(
|
||||
ctx: CredentialsContext,
|
||||
buildProfile: BuildProfile<Platform.ANDROID>
|
||||
): Promise<GradleBuildContext | undefined> {
|
||||
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
|
||||
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
|
||||
if (!maybeProjectId) {
|
||||
throw new Error(
|
||||
'Your project must be registered with EAS in order to use the credentials manager.'
|
||||
);
|
||||
}
|
||||
|
||||
const buildProfile = await new SelectBuildProfileFromEasJson(
|
||||
ctx.projectDir,
|
||||
Platform.ANDROID
|
||||
).runAsync(ctx);
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
return {
|
||||
gradleContext,
|
||||
buildProfile,
|
||||
};
|
||||
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
}
|
||||
|
||||
private async runProjectSpecificActionAsync(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { IosBuildProfile } from '@expo/eas-json';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { Account, findAccountByName } from '../../user/Account';
|
||||
import { ensureActorHasUsername } from '../../user/actions';
|
||||
import { ensureActorHasUsername, ensureLoggedInAsync } from '../../user/actions';
|
||||
import { CredentialsContext } from '../context';
|
||||
import {
|
||||
AppStoreApiKeyPurpose,
|
||||
@@ -59,12 +59,18 @@ import { SelectBuildProfileFromEasJson } from './SelectBuildProfileFromEasJson';
|
||||
import { SelectIosDistributionTypeGraphqlFromBuildProfile } from './SelectIosDistributionTypeGraphqlFromBuildProfile';
|
||||
|
||||
export class ManageIos {
|
||||
constructor(private callingAction: Action) {}
|
||||
constructor(private callingAction: Action, private projectDir: string) {}
|
||||
|
||||
async runAsync(
|
||||
ctx: CredentialsContext,
|
||||
currentActions: ActionInfo[] = highLevelActions
|
||||
): Promise<void> {
|
||||
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
|
||||
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir);
|
||||
const buildProfile = hasProjectContext
|
||||
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
|
||||
: null;
|
||||
const ctx = new CredentialsContext({
|
||||
projectDir: process.cwd(),
|
||||
user: await ensureLoggedInAsync(),
|
||||
env: buildProfile?.env,
|
||||
});
|
||||
const buildCredentialsActions = getBuildCredentialsActions(ctx);
|
||||
const pushKeyActions = getPushKeyActions(ctx);
|
||||
const ascApiKeyActions = getAscApiKeyActions(ctx);
|
||||
@@ -78,7 +84,16 @@ export class ManageIos {
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to account: ${accountName}`);
|
||||
}
|
||||
const { app, targets, buildProfile } = await this.createProjectContextAsync(ctx, account);
|
||||
|
||||
let app = null;
|
||||
let targets = null;
|
||||
if (ctx.hasProjectContext) {
|
||||
assert(buildProfile, 'buildProfile must be defined in project context');
|
||||
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
|
||||
app = projectContext.app;
|
||||
targets = projectContext.targets;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
if (ctx.hasProjectContext) {
|
||||
@@ -154,16 +169,13 @@ export class ManageIos {
|
||||
|
||||
private async createProjectContextAsync(
|
||||
ctx: CredentialsContext,
|
||||
account: Account
|
||||
): Promise<{ app: App | null; targets: Target[] | null; buildProfile: IosBuildProfile | null }> {
|
||||
if (!ctx.hasProjectContext) {
|
||||
return {
|
||||
app: null,
|
||||
targets: null,
|
||||
buildProfile: null,
|
||||
};
|
||||
}
|
||||
|
||||
account: Account,
|
||||
buildProfile: BuildProfile<Platform.IOS>
|
||||
): Promise<{
|
||||
app: App;
|
||||
targets: Target[];
|
||||
}> {
|
||||
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
|
||||
const maybeProjectId = await promptToCreateProjectIfNotExistsAsync(ctx.exp);
|
||||
if (!maybeProjectId) {
|
||||
throw new Error(
|
||||
@@ -172,10 +184,6 @@ export class ManageIos {
|
||||
}
|
||||
|
||||
const app = { account, projectName: ctx.exp.slug };
|
||||
const buildProfile = await new SelectBuildProfileFromEasJson(
|
||||
ctx.projectDir,
|
||||
Platform.IOS
|
||||
).runAsync(ctx);
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
@@ -191,7 +199,6 @@ export class ManageIos {
|
||||
return {
|
||||
app,
|
||||
targets,
|
||||
buildProfile,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,7 +226,7 @@ export class ManageIos {
|
||||
ctx: CredentialsContext,
|
||||
app: App,
|
||||
targets: Target[],
|
||||
buildProfile: IosBuildProfile,
|
||||
buildProfile: BuildProfile<Platform.IOS>,
|
||||
action: IosActionType
|
||||
): Promise<void> {
|
||||
if (action === IosActionType.SetUpBuildCredentials) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { BuildProfile, EasJsonReader } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { CredentialsContext } from '../context';
|
||||
|
||||
export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
private easJsonReader: EasJsonReader;
|
||||
@@ -12,14 +11,14 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
this.easJsonReader = new EasJsonReader(projectDir);
|
||||
}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<BuildProfile<T>> {
|
||||
const profileName = await this.getProfileNameFromEasConfigAsync(ctx);
|
||||
const easConfig = await this.easJsonReader.readBuildProfileAsync<T>(this.platform, profileName);
|
||||
async runAsync(): Promise<BuildProfile<T>> {
|
||||
const profileName = await this.getProfileNameFromEasConfigAsync();
|
||||
const easConfig = await this.easJsonReader.getBuildProfileAsync<T>(this.platform, profileName);
|
||||
Log.succeed(`Using build profile: ${profileName}`);
|
||||
return easConfig;
|
||||
}
|
||||
|
||||
async getProfileNameFromEasConfigAsync(ctx: CredentialsContext): Promise<string> {
|
||||
async getProfileNameFromEasConfigAsync(): Promise<string> {
|
||||
const buildProfileNames = await this.easJsonReader.getBuildProfileNamesAsync();
|
||||
if (buildProfileNames.length === 0) {
|
||||
throw new Error(
|
||||
@@ -28,11 +27,6 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
} else if (buildProfileNames.length === 1) {
|
||||
return buildProfileNames[0];
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'You have multiple build profiles. Please run this command in interactive mode.'
|
||||
);
|
||||
}
|
||||
const { profileName } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'profileName',
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import { IosBuildProfile } from '@expo/eas-json';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { IosDistributionType as IosDistributionTypeGraphql } from '../../graphql/generated';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { CredentialsContext } from '../context';
|
||||
|
||||
export class SelectIosDistributionTypeGraphqlFromBuildProfile {
|
||||
constructor(private buildProfile: IosBuildProfile) {}
|
||||
constructor(private buildProfile: BuildProfile<Platform.IOS>) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosDistributionTypeGraphql> {
|
||||
const { distribution, simulator } = this.buildProfile;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { CredentialsContext } from '../context';
|
||||
import { ManageAndroid } from './ManageAndroid';
|
||||
import { ManageIos } from './ManageIos';
|
||||
|
||||
export class SelectPlatform {
|
||||
async runAsync(ctx: CredentialsContext): Promise<void> {
|
||||
async runAsync(): Promise<void> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'platform',
|
||||
@@ -16,8 +15,8 @@ export class SelectPlatform {
|
||||
});
|
||||
|
||||
if (platform === 'ios') {
|
||||
return await new ManageIos(new SelectPlatform()).runAsync(ctx);
|
||||
return await new ManageIos(new SelectPlatform(), process.cwd()).runAsync();
|
||||
}
|
||||
return await new ManageAndroid(new SelectPlatform()).runAsync(ctx);
|
||||
return await new ManageAndroid(new SelectPlatform(), process.cwd()).runAsync();
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -1,4 +1,5 @@
|
||||
import { IosBuildProfile } from '@expo/eas-json';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { IosDistributionType as IosDistributionTypeGraphql } from '../../../graphql/generated';
|
||||
import { createCtxMock } from '../../__tests__/fixtures-context';
|
||||
@@ -8,7 +9,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
it('errors with a simulator distribution', async () => {
|
||||
const buildProfile = {
|
||||
simulator: true,
|
||||
} as IosBuildProfile;
|
||||
} as BuildProfile<Platform.IOS>;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
@@ -22,7 +23,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
it('returns APP_STORE type with a store distribution', async () => {
|
||||
const buildProfile = {
|
||||
distribution: 'store',
|
||||
} as IosBuildProfile;
|
||||
} as BuildProfile<Platform.IOS>;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
@@ -37,7 +38,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
const buildProfile = {
|
||||
distribution: 'internal',
|
||||
enterpriseProvisioning: 'universal',
|
||||
} as IosBuildProfile;
|
||||
} as BuildProfile<Platform.IOS>;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
@@ -52,7 +53,7 @@ describe('SelectIosDistributionTypeGraphqlFromBuildProfile', () => {
|
||||
const buildProfile = {
|
||||
distribution: 'internal',
|
||||
enterpriseProvisioning: 'adhoc',
|
||||
} as IosBuildProfile;
|
||||
} as BuildProfile<Platform.IOS>;
|
||||
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
|
||||
+38
-15
@@ -720,6 +720,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
appBuildVersion?: Maybe<Scalars['String']>;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
reactNativeVersion?: Maybe<Scalars['String']>;
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
metrics?: Maybe<BuildMetrics>;
|
||||
@@ -1411,7 +1412,8 @@ export type WebhookFilter = {
|
||||
};
|
||||
|
||||
export enum WebhookType {
|
||||
Build = 'BUILD'
|
||||
Build = 'BUILD',
|
||||
Submit = 'SUBMIT'
|
||||
}
|
||||
|
||||
export type Webhook = {
|
||||
@@ -1771,6 +1773,7 @@ export type InvoiceQuery = {
|
||||
export type InvoiceQueryPreviewInvoiceForSubscriptionUpdateArgs = {
|
||||
accountId: Scalars['String'];
|
||||
newPlanIdentifier: Scalars['String'];
|
||||
couponCode?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type Invoice = {
|
||||
@@ -1782,6 +1785,8 @@ export type Invoice = {
|
||||
amountPaid: Scalars['Int'];
|
||||
/** The total amount that needs to be paid, considering any discounts or account credit. Value is in cents. */
|
||||
amountRemaining: Scalars['Int'];
|
||||
discount?: Maybe<InvoiceDiscount>;
|
||||
totalDiscountedAmount: Scalars['Int'];
|
||||
lineItems: Array<InvoiceLineItem>;
|
||||
period: InvoicePeriod;
|
||||
startingBalance: Scalars['Int'];
|
||||
@@ -1789,6 +1794,22 @@ export type Invoice = {
|
||||
total: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type InvoiceDiscount = {
|
||||
__typename?: 'InvoiceDiscount';
|
||||
id: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
type: InvoiceDiscountType;
|
||||
duration: Scalars['String'];
|
||||
durationInMonths?: Maybe<Scalars['Int']>;
|
||||
/** The coupon's discount value, in percentage or in dollar amount */
|
||||
amount: Scalars['Int'];
|
||||
};
|
||||
|
||||
export enum InvoiceDiscountType {
|
||||
Percentage = 'PERCENTAGE',
|
||||
Amount = 'AMOUNT'
|
||||
}
|
||||
|
||||
export type InvoiceLineItem = {
|
||||
__typename?: 'InvoiceLineItem';
|
||||
id: Scalars['ID'];
|
||||
@@ -2114,6 +2135,7 @@ export type AccountMutationCancelSubscriptionArgs = {
|
||||
export type AccountMutationChangePlanArgs = {
|
||||
accountID: Scalars['ID'];
|
||||
newPlanIdentifier: Scalars['String'];
|
||||
couponCode?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -2743,6 +2765,7 @@ export type BuildMetadataInput = {
|
||||
credentialsSource?: Maybe<BuildCredentialsSource>;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
reactNativeVersion?: Maybe<Scalars['String']>;
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
distribution?: Maybe<DistributionType>;
|
||||
@@ -3680,19 +3703,6 @@ export type BranchesByAppQuery = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetUpdateGroupAsyncQueryVariables = Exact<{
|
||||
group: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetUpdateGroupAsyncQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { updatesByGroup: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'runtimeVersion' | 'manifestFragment' | 'platform' | 'message'>
|
||||
)> }
|
||||
);
|
||||
|
||||
export type EditUpdateBranchMutationVariables = Exact<{
|
||||
input: EditUpdateBranchInput;
|
||||
}>;
|
||||
@@ -3923,6 +3933,19 @@ export type DeleteUpdateGroupMutation = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetUpdateGroupAsyncQueryVariables = Exact<{
|
||||
group: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetUpdateGroupAsyncQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { updatesByGroup: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'runtimeVersion' | 'manifestFragment' | 'platform' | 'message'>
|
||||
)> }
|
||||
);
|
||||
|
||||
export type UpdatesByGroupQueryVariables = Exact<{
|
||||
groupId: Scalars['ID'];
|
||||
}>;
|
||||
@@ -5448,7 +5471,7 @@ export type SubmissionFragment = (
|
||||
& Pick<Submission, 'id' | 'status' | 'platform' | 'logsUrl'>
|
||||
& { app: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id' | 'name'>
|
||||
& Pick<App, 'id' | 'name' | 'slug'>
|
||||
& { ownerAccount: (
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id' | 'name'>
|
||||
|
||||
@@ -8,6 +8,7 @@ export const SubmissionFragmentNode = gql`
|
||||
app {
|
||||
id
|
||||
name
|
||||
slug
|
||||
ownerAccount {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -3,7 +3,7 @@ import mockdate from 'mockdate';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { defaultPublishPlatforms } from '../../commands/branch/publish';
|
||||
import { defaultPublishPlatforms } from '../../commands/update';
|
||||
import { AssetMetadataStatus } from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { exit } from '@oclif/errors';
|
||||
import assert from 'assert';
|
||||
import semver from 'semver';
|
||||
|
||||
import { BuildContext } from '../build/context';
|
||||
import Log from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
|
||||
const SUPPORTED_EXPO_SDK_VERSIONS = '>= 41.0.0';
|
||||
assert(semver.validRange(SUPPORTED_EXPO_SDK_VERSIONS), 'Must be a valid version range');
|
||||
|
||||
export async function checkExpoSdkIsSupportedAsync(ctx: BuildContext<Platform>): Promise<void> {
|
||||
assert(ctx.workflow === Workflow.MANAGED, 'Must be a managed workflow project');
|
||||
|
||||
if (ctx.exp.sdkVersion && semver.satisfies(ctx.exp.sdkVersion, SUPPORTED_EXPO_SDK_VERSIONS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsupportedSdkMessage =
|
||||
'EAS Build does not officially support building managed project with Expo SDK < 41.';
|
||||
if (ctx.nonInteractive) {
|
||||
Log.warn(
|
||||
`${unsupportedSdkMessage} Proceeding because you are running in non-interactive mode.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const proceed = await confirmAsync({
|
||||
message: `${unsupportedSdkMessage} Do you want to proceed?`,
|
||||
});
|
||||
if (!proceed) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { IosBuildProfile } from '@expo/eas-json';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
@@ -20,7 +20,7 @@ export async function resolveXcodeBuildContextAsync(
|
||||
projectDir,
|
||||
nonInteractive,
|
||||
}: { exp: ExpoConfig; projectDir: string; nonInteractive: boolean },
|
||||
buildProfile: IosBuildProfile
|
||||
buildProfile: BuildProfile<Platform.IOS>
|
||||
): Promise<XcodeBuildContext> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
|
||||
@@ -24,17 +24,57 @@ export async function resolveTargetsAsync(
|
||||
buildConfiguration,
|
||||
});
|
||||
|
||||
if (applicationTarget.dependencies && applicationTarget.dependencies.length > 0) {
|
||||
for (const dependency of applicationTarget.dependencies) {
|
||||
const dependencies = await resolveDependenciesAsync({
|
||||
exp,
|
||||
projectDir,
|
||||
buildConfiguration,
|
||||
target: applicationTarget,
|
||||
bundleIdentifier,
|
||||
});
|
||||
if (dependencies.length > 0) {
|
||||
result.push(...dependencies);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function resolveDependenciesAsync({
|
||||
exp,
|
||||
projectDir,
|
||||
buildConfiguration,
|
||||
target,
|
||||
bundleIdentifier,
|
||||
}: {
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
buildConfiguration?: string;
|
||||
target: IOSConfig.Target.Target;
|
||||
bundleIdentifier: string;
|
||||
}): Promise<Target[]> {
|
||||
const result: Target[] = [];
|
||||
|
||||
if (target.dependencies && target.dependencies.length > 0) {
|
||||
for (const dependency of target.dependencies) {
|
||||
const dependencyBundleIdentifier = await getBundleIdentifierAsync(projectDir, exp, {
|
||||
targetName: dependency.name,
|
||||
buildConfiguration,
|
||||
});
|
||||
result.push({
|
||||
targetName: dependency.name,
|
||||
buildConfiguration,
|
||||
bundleIdentifier: await getBundleIdentifierAsync(projectDir, exp, {
|
||||
targetName: dependency.name,
|
||||
buildConfiguration,
|
||||
}),
|
||||
bundleIdentifier: dependencyBundleIdentifier,
|
||||
parentBundleIdentifier: bundleIdentifier,
|
||||
});
|
||||
const dependencyDependencies = await resolveDependenciesAsync({
|
||||
exp,
|
||||
projectDir,
|
||||
buildConfiguration,
|
||||
target: dependency,
|
||||
bundleIdentifier: dependencyBundleIdentifier,
|
||||
});
|
||||
if (dependencyDependencies.length > 0) {
|
||||
result.push(...dependencyDependencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ import prompts, { Answers, Choice, Options, PromptType, PromptObject as Question
|
||||
|
||||
export { PromptType, Question, Choice };
|
||||
|
||||
export interface ExpoChoice<T> extends Choice {
|
||||
value: T;
|
||||
}
|
||||
|
||||
type NamelessQuestion = Omit<Question<'value'>, 'name' | 'type'>;
|
||||
|
||||
export async function promptAsync<T extends string = string>(
|
||||
@@ -35,11 +39,6 @@ export async function confirmAsync(
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
interface ExpoChoice<T> extends Choice {
|
||||
value: T;
|
||||
}
|
||||
|
||||
export async function selectAsync<T>(
|
||||
message: string,
|
||||
choices: ExpoChoice<T>[],
|
||||
|
||||
@@ -128,7 +128,8 @@ export default class AndroidSubmitCommand {
|
||||
path: serviceAccountKeyPath,
|
||||
});
|
||||
}
|
||||
let androidApplicationIdentifier: string | undefined = this.ctx.profile.applicationId;
|
||||
let androidApplicationIdentifier: string | undefined =
|
||||
this.ctx.applicationIdentifierOverride ?? this.ctx.profile.applicationId;
|
||||
if (!androidApplicationIdentifier) {
|
||||
const androidApplicationIdentifierResult =
|
||||
await this.maybeGetAndroidPackageFromCurrentProjectAsync();
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface SubmissionContext<T extends Platform> {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
user: Actor;
|
||||
applicationIdentifierOverride?: string;
|
||||
}
|
||||
|
||||
export interface SubmitArchiveFlags {
|
||||
@@ -43,8 +44,9 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
profile: SubmitProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
applicationIdentifier?: string;
|
||||
}): Promise<SubmissionContext<T>> {
|
||||
const { projectDir, nonInteractive } = params;
|
||||
const { applicationIdentifier, projectDir, nonInteractive } = params;
|
||||
const exp = getExpoConfig(projectDir, { env: params.env });
|
||||
const { env, ...rest } = params;
|
||||
const user = await ensureLoggedInAsync();
|
||||
@@ -60,7 +62,6 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
tracking_id: uuidv4(),
|
||||
platform: params.platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: accountName,
|
||||
project_id: params.projectId,
|
||||
};
|
||||
|
||||
@@ -74,5 +75,6 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
|
||||
projectName,
|
||||
user,
|
||||
trackingCtx,
|
||||
applicationIdentifierOverride: applicationIdentifier,
|
||||
};
|
||||
}
|
||||
@@ -35,7 +35,10 @@ export async function ensureAppStoreConnectAppExistsAsync(
|
||||
const { appName, language } = ctx.profile;
|
||||
const options = {
|
||||
...ctx.profile,
|
||||
bundleIdentifier: await getBundleIdentifierAsync(ctx.projectDir, exp),
|
||||
bundleIdentifier:
|
||||
ctx.applicationIdentifierOverride ??
|
||||
ctx.profile.bundleIdentifier ??
|
||||
(await getBundleIdentifierAsync(ctx.projectDir, exp)),
|
||||
appName: appName ?? exp.name ?? (await promptForAppNameAsync()),
|
||||
language: sanitizeLanguage(language),
|
||||
};
|
||||
|
||||
@@ -119,7 +119,8 @@ export default class IosSubmitCommand {
|
||||
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
|
||||
}
|
||||
|
||||
let bundleIdentifier = this.ctx.profile.bundleIdentifier;
|
||||
let bundleIdentifier =
|
||||
this.ctx.applicationIdentifierOverride ?? this.ctx.profile.bundleIdentifier;
|
||||
if (!bundleIdentifier) {
|
||||
const bundleIdentifierResult = await this.maybeGetIosBundleIdentifierAsync();
|
||||
if (!bundleIdentifierResult.ok) {
|
||||
@@ -173,7 +174,14 @@ export default class IosSubmitCommand {
|
||||
|
||||
// interpret this to mean the user had some intention of passing in ASC Api key
|
||||
if (ascApiKeyPath || ascApiKeyIssuerId || ascApiKeyId) {
|
||||
Log.warn(`ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId must all be defined in eas.json`);
|
||||
const message = `ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId must all be defined in eas.json`;
|
||||
|
||||
// in non-interactive mode, we should fail
|
||||
if (this.ctx.nonInteractive) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
Log.warn(message);
|
||||
return result({
|
||||
sourceType: AscApiKeySourceType.prompt,
|
||||
});
|
||||
@@ -199,7 +207,11 @@ export default class IosSubmitCommand {
|
||||
if (ascAppId) {
|
||||
return result(ascAppId);
|
||||
} else if (this.ctx.nonInteractive) {
|
||||
return result(new Error('Set ascAppId in the submit profile (eas.json).'));
|
||||
return result(
|
||||
new Error(
|
||||
'Set ascAppId in the submit profile (eas.json) or re-run this command in interactive mode.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
wrapAnsi(
|
||||
|
||||
@@ -24,7 +24,7 @@ export function printSubmissionDetailsUrls(submissions: SubmissionFragment[]): v
|
||||
export function getSubmissionDetailsUrl(submission: SubmissionFragment): string {
|
||||
const { id, app } = submission;
|
||||
return new URL(
|
||||
`/accounts/${app.ownerAccount.name}/projects/${app.name}/submissions/${id}`,
|
||||
`/accounts/${app.ownerAccount.name}/projects/${app.slug}/submissions/${id}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { sleepAsync } from '../../utils/promise';
|
||||
|
||||
const APP_STORE_NAMES: Record<AppPlatform, string> = {
|
||||
[AppPlatform.Android]: 'Google Play Store',
|
||||
[AppPlatform.Ios]: 'Apple App Store',
|
||||
[AppPlatform.Ios]: 'Apple App Store Connect',
|
||||
};
|
||||
|
||||
const CHECK_TIMEOUT_MS = 3600_000;
|
||||
|
||||
@@ -9,10 +9,10 @@ export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'>
|
||||
};
|
||||
|
||||
export const UPDATE_COLUMNS = [
|
||||
'update description',
|
||||
'update runtime version',
|
||||
'update group ID',
|
||||
'platforms',
|
||||
'Update message',
|
||||
'Update runtime version',
|
||||
'Update group ID',
|
||||
'Update platforms',
|
||||
];
|
||||
|
||||
export function getPlatformsForGroup({
|
||||
@@ -29,7 +29,7 @@ export function getPlatformsForGroup({
|
||||
return groupedUpdates[group]
|
||||
.map(update => update.platform)
|
||||
.sort()
|
||||
.join(',');
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
export function formatUpdate(update: FormatUpdateParameter): string {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader, errors } from '@expo/eas-json';
|
||||
import { BuildProfile, EasJsonReader, errors } from '@expo/eas-json';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { getProfilesAsync } from '../profiles';
|
||||
|
||||
jest.mock('../../prompts');
|
||||
jest.mock('@expo/eas-json', () => {
|
||||
const actual = jest.requireActual('@expo/eas-json');
|
||||
|
||||
const EasJsonReaderMock = jest.fn();
|
||||
EasJsonReaderMock.prototype = {
|
||||
readBuildProfileAsync: jest.fn(),
|
||||
readSubmitProfileAsync: jest.fn(),
|
||||
getBuildProfileAsync: jest.fn(),
|
||||
getBuildProfileNamesAsync: jest.fn(),
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
@@ -17,11 +20,14 @@ jest.mock('@expo/eas-json', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const readBuildProfileAsync = jest.spyOn(EasJsonReader.prototype, 'readBuildProfileAsync');
|
||||
const getBuildProfileAsync = jest.spyOn(EasJsonReader.prototype, 'getBuildProfileAsync');
|
||||
const getBuildProfileNamesAsync = jest.spyOn(EasJsonReader.prototype, 'getBuildProfileNamesAsync');
|
||||
|
||||
describe(getProfilesAsync, () => {
|
||||
afterEach(() => {
|
||||
readBuildProfileAsync.mockReset();
|
||||
getBuildProfileAsync.mockReset();
|
||||
getBuildProfileNamesAsync.mockReset();
|
||||
asMock(selectAsync).mockReset();
|
||||
});
|
||||
|
||||
it('defaults to production profile', async () => {
|
||||
@@ -34,17 +40,17 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
expect(result[0].profileName).toBe('production');
|
||||
expect(result[1].profileName).toBe('production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
});
|
||||
|
||||
it('defaults to release profile when production profile is non-existent', async () => {
|
||||
readBuildProfileAsync
|
||||
.mockRejectedValueOnce(() => {
|
||||
throw new Error();
|
||||
getBuildProfileAsync
|
||||
.mockImplementationOnce(() => {
|
||||
throw new errors.MissingProfileError();
|
||||
})
|
||||
.mockRejectedValueOnce(() => {
|
||||
throw new Error();
|
||||
.mockImplementationOnce(() => {
|
||||
throw new errors.MissingProfileError();
|
||||
});
|
||||
|
||||
const result = await getProfilesAsync({
|
||||
@@ -56,20 +62,38 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
expect(result[0].profileName).toBe('release');
|
||||
expect(result[1].profileName).toBe('release');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'release');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'release');
|
||||
});
|
||||
|
||||
it('fails when neither production or release profiles are present', async () => {
|
||||
readBuildProfileAsync
|
||||
.mockRejectedValueOnce(() => {
|
||||
throw new Error();
|
||||
})
|
||||
.mockRejectedValueOnce(() => {
|
||||
throw new Error();
|
||||
});
|
||||
it('asks the user to pick profile if "release" does not exist', async () => {
|
||||
getBuildProfileAsync.mockImplementation(async (_, profileName) => {
|
||||
if (profileName === 'foo') {
|
||||
return {} as unknown as BuildProfile<Platform>;
|
||||
}
|
||||
throw new errors.MissingProfileError();
|
||||
});
|
||||
getBuildProfileNamesAsync.mockImplementation(() => Promise.resolve(['foo', 'bar']));
|
||||
asMock(selectAsync).mockImplementation(() => 'foo');
|
||||
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
});
|
||||
expect(selectAsync).toHaveBeenCalled();
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'foo');
|
||||
expect(result[0].profileName).toBe('foo');
|
||||
});
|
||||
|
||||
it('throws an error if there are no profiles in eas.json', async () => {
|
||||
getBuildProfileAsync.mockImplementation(async () => {
|
||||
throw new errors.MissingProfileError();
|
||||
});
|
||||
getBuildProfileNamesAsync.mockImplementation(() => Promise.resolve([]));
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
@@ -78,7 +102,7 @@ describe(getProfilesAsync, () => {
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
})
|
||||
).rejects.toThrowError(/There is no build profile named "production" in eas.json/);
|
||||
).rejects.toThrowError(errors.MissingProfileError);
|
||||
});
|
||||
|
||||
it('gets a specific profile', async () => {
|
||||
@@ -91,12 +115,12 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
expect(result[0].profileName).toBe('custom-profile');
|
||||
expect(result[1].profileName).toBe('custom-profile');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
|
||||
expect(readBuildProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'custom-profile');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'custom-profile');
|
||||
});
|
||||
|
||||
it('throws validation error if eas.json is invalid', async () => {
|
||||
readBuildProfileAsync.mockImplementation(() => {
|
||||
getBuildProfileAsync.mockImplementation(() => {
|
||||
throw new errors.InvalidEasJsonError('eas.json is not valid');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJsonReader, SubmitProfile, errors } from '@expo/eas-json';
|
||||
import {
|
||||
BuildProfile,
|
||||
EasJsonReader,
|
||||
ProfileType,
|
||||
SubmitProfile,
|
||||
errors,
|
||||
getDefaultSubmitProfile,
|
||||
} from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
type ProfileType = 'build' | 'submit';
|
||||
import { ExpoChoice, selectAsync } from '../prompts';
|
||||
|
||||
type EasProfile<T extends ProfileType> = T extends 'build'
|
||||
? BuildProfile<Platform>
|
||||
@@ -18,46 +25,107 @@ export type ProfileData<T extends ProfileType> = {
|
||||
export async function getProfilesAsync<T extends ProfileType>({
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: profileNameArg,
|
||||
profileName,
|
||||
type,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platforms: Platform[];
|
||||
profileName?: string | null;
|
||||
profileName?: string;
|
||||
type: T;
|
||||
}): Promise<ProfileData<T>[]> {
|
||||
const results = platforms.map(async function (platform) {
|
||||
let profile: EasProfile<T>;
|
||||
let profileName = profileNameArg;
|
||||
|
||||
if (!profileName) {
|
||||
try {
|
||||
profile = await readProfileAsync({ projectDir, platform, type, profileName: 'production' });
|
||||
profileName = 'production';
|
||||
} catch (errorOuter) {
|
||||
if (errorOuter instanceof errors.InvalidEasJsonError) {
|
||||
throw errorOuter;
|
||||
}
|
||||
try {
|
||||
profile = await readProfileAsync({ projectDir, platform, type, profileName: 'release' });
|
||||
profileName = 'release';
|
||||
Log.warn(
|
||||
'The default profile changed from "release" to "production". We detected that you still have a "release" build profile, so we are using it. Update eas.json to have a profile named "production" under the `build` key, or specify which profile you\'d like to use with the --profile flag. This fallback behavior will be removed in the next major version of EAS CLI.'
|
||||
);
|
||||
} catch (errorInner) {
|
||||
if (errorInner instanceof errors.InvalidEasJsonError) {
|
||||
throw errorInner;
|
||||
}
|
||||
throw new Error(`There is no ${type} profile named "production" in eas.json`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
profile = await readProfileAsync({ projectDir, platform, type, profileName });
|
||||
if (profileName) {
|
||||
const profile = await readProfileAsync({ projectDir, platform, type, profileName });
|
||||
return {
|
||||
profile,
|
||||
profileName,
|
||||
platform,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await readProfileAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
type,
|
||||
profileName: 'production',
|
||||
});
|
||||
return {
|
||||
profile,
|
||||
profileName: 'production',
|
||||
platform,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof errors.MissingProfileError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await readProfileAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
type,
|
||||
profileName: 'release',
|
||||
});
|
||||
Log.warn(
|
||||
`The default profile changed from ${chalk.bold('release')} to ${chalk.bold(
|
||||
'production'
|
||||
)}. We detected that you still have a ${chalk.bold(
|
||||
'release'
|
||||
)} build profile, so we are using it. Update eas.json to have a profile named ${chalk.bold(
|
||||
'production'
|
||||
)} under the ${chalk.bold(
|
||||
'build'
|
||||
)} key, or specify which profile you'd like to use with the ${chalk.bold(
|
||||
'--profile'
|
||||
)} flag. This fallback behavior will be removed in the next major version of EAS CLI.`
|
||||
);
|
||||
return {
|
||||
profile,
|
||||
profileName: 'release',
|
||||
platform,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof errors.MissingProfileError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const defaultProfile = getDefaultProfile({ platform, type });
|
||||
if (defaultProfile) {
|
||||
return {
|
||||
profile: defaultProfile,
|
||||
profileName: '__default__',
|
||||
platform,
|
||||
};
|
||||
}
|
||||
|
||||
const profileNames = await readProfileNamesAsync({
|
||||
projectDir,
|
||||
type,
|
||||
});
|
||||
if (profileNames.length === 0) {
|
||||
throw new errors.MissingProfileError(
|
||||
`Missing profile in eas.json: ${profileName ?? 'production'}`
|
||||
);
|
||||
}
|
||||
const choices: ExpoChoice<string>[] = profileNames.map(profileName => ({
|
||||
title: profileName,
|
||||
value: profileName,
|
||||
}));
|
||||
const chosenProfileName = await selectAsync(
|
||||
'The "production" profile is missing in eas.json. Pick another profile:',
|
||||
choices
|
||||
);
|
||||
const profile = await readProfileAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
type,
|
||||
profileName: chosenProfileName,
|
||||
});
|
||||
return {
|
||||
profile,
|
||||
profileName,
|
||||
profileName: chosenProfileName,
|
||||
platform,
|
||||
};
|
||||
});
|
||||
@@ -73,13 +141,42 @@ async function readProfileAsync<T extends ProfileType>({
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
type: ProfileType;
|
||||
type: T;
|
||||
profileName: string;
|
||||
}): Promise<EasProfile<T>> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
if (type === 'build') {
|
||||
return (await easJsonReader.readBuildProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
return (await easJsonReader.getBuildProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
} else {
|
||||
return (await easJsonReader.readSubmitProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
return (await easJsonReader.getSubmitProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultProfile<T extends ProfileType>({
|
||||
platform,
|
||||
type,
|
||||
}: {
|
||||
platform: Platform;
|
||||
type: T;
|
||||
}): EasProfile<T> | null {
|
||||
if (type === 'build') {
|
||||
return null;
|
||||
} else {
|
||||
return getDefaultSubmitProfile(platform) as EasProfile<T>;
|
||||
}
|
||||
}
|
||||
|
||||
async function readProfileNamesAsync({
|
||||
projectDir,
|
||||
type,
|
||||
}: {
|
||||
projectDir: string;
|
||||
type: ProfileType;
|
||||
}): Promise<string[]> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
if (type === 'build') {
|
||||
return await easJsonReader.getBuildProfileNamesAsync();
|
||||
} else {
|
||||
return await easJsonReader.getSubmitProfileNamesAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { exit } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log, { learnMore } from '../../log';
|
||||
@@ -16,7 +17,13 @@ import { Client } from '../vcs';
|
||||
export default class GitClient extends Client {
|
||||
public async ensureRepoExistsAsync(): Promise<void> {
|
||||
if (!(await isGitInstalledAsync())) {
|
||||
throw new Error('git command not found, install it before proceeding');
|
||||
Log.error(
|
||||
`${chalk.bold('git')} command not found. Install it before proceeding or set ${chalk.bold(
|
||||
'EAS_NO_VCS=1'
|
||||
)} to use EAS CLI without Git (or any other version control system).`
|
||||
);
|
||||
Log.error(learnMore('https://expo.fyi/eas-vcs-workflow'));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (await doesGitRepoExistAsync()) {
|
||||
|
||||
@@ -7,19 +7,36 @@ import { promptAsync } from '../prompts';
|
||||
|
||||
export async function prepareInputParamsAsync(
|
||||
{
|
||||
event,
|
||||
event: maybeEvent,
|
||||
url: maybeUrl,
|
||||
secret: maybeSecret,
|
||||
}: {
|
||||
event: WebhookType;
|
||||
event?: WebhookType;
|
||||
url?: string;
|
||||
secret?: string;
|
||||
},
|
||||
existingWebhook?: WebhookFragment
|
||||
): Promise<WebhookInput> {
|
||||
let event: WebhookType | undefined = maybeEvent;
|
||||
let url: string | undefined = maybeUrl;
|
||||
let secret: string | undefined = maybeSecret;
|
||||
|
||||
if (!event) {
|
||||
const choices = [
|
||||
{ title: 'Build', value: WebhookType.Build },
|
||||
{ title: 'Submit', value: WebhookType.Submit },
|
||||
];
|
||||
({ event } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'event',
|
||||
message: 'Webhook event type:',
|
||||
choices,
|
||||
initial: existingWebhook?.event
|
||||
? choices.findIndex(choice => choice.value === existingWebhook.event)
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!url || !validateURL(url)) {
|
||||
const urlValidationMessage =
|
||||
'The provided webhook URL is invalid and must be an absolute URL, including a scheme.';
|
||||
@@ -50,7 +67,7 @@ export async function prepareInputParamsAsync(
|
||||
}
|
||||
|
||||
return {
|
||||
event,
|
||||
event: nullthrows(event),
|
||||
url: nullthrows(url),
|
||||
secret: nullthrows(secret),
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,22 +1,24 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "0.37.0",
|
||||
"version": "0.41.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "0.2.57",
|
||||
"@expo/eas-build-job": "0.2.61",
|
||||
"@expo/json-file": "8.2.33",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "10.0.0",
|
||||
"joi": "17.4.2",
|
||||
"log-symbols": "4.1.0",
|
||||
"semver": "7.3.5",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "9.0.12",
|
||||
"memfs": "3.2.2"
|
||||
"memfs": "3.2.2",
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidBuildProfile, CommonBuildProfile, IosBuildProfile } from './EasBuild.types';
|
||||
import { AndroidSubmitProfile, IosSubmitProfile } from './EasSubmit.types';
|
||||
|
||||
export enum CredentialsSource {
|
||||
LOCAL = 'local',
|
||||
REMOTE = 'remote',
|
||||
}
|
||||
|
||||
export interface RawBuildProfile extends Partial<CommonBuildProfile> {
|
||||
extends?: string;
|
||||
[Platform.ANDROID]?: Partial<AndroidBuildProfile>;
|
||||
[Platform.IOS]?: Partial<IosBuildProfile>;
|
||||
}
|
||||
|
||||
export interface EasSubmitConfiguration {
|
||||
[Platform.ANDROID]?: AndroidSubmitProfile;
|
||||
[Platform.IOS]?: IosSubmitProfile;
|
||||
}
|
||||
|
||||
export interface CliConfig {
|
||||
version?: string;
|
||||
requireCommit?: boolean;
|
||||
}
|
||||
|
||||
export interface EasJson {
|
||||
cli?: CliConfig;
|
||||
build: { [profile: string]: RawBuildProfile };
|
||||
submit?: { [profile: string]: EasSubmitConfiguration };
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import envString from 'env-string';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildProfile } from './EasBuild.types';
|
||||
import { CliConfig, CredentialsSource, EasJson, RawBuildProfile } from './EasJson.types';
|
||||
import {
|
||||
AndroidSubmitProfileSchema,
|
||||
CliConfigSchema,
|
||||
EasJsonSchema,
|
||||
IosSubmitProfileSchema,
|
||||
MinimalEasJsonSchema,
|
||||
} from './EasJsonSchema';
|
||||
import {
|
||||
AndroidSubmitProfileFieldsToEvaluate,
|
||||
IosSubmitProfileFieldsToEvaluate,
|
||||
SubmitProfile,
|
||||
} from './EasSubmit.types';
|
||||
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
|
||||
|
||||
interface EasJsonPreValidation {
|
||||
cli?: object;
|
||||
build: { [profile: string]: object };
|
||||
submit?: { [profile: string]: object };
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
distribution: 'store',
|
||||
credentialsSource: CredentialsSource.REMOTE,
|
||||
} as const;
|
||||
|
||||
type LoggerFn = (...args: any[]) => void;
|
||||
|
||||
interface Logger {
|
||||
log: LoggerFn;
|
||||
warn: LoggerFn;
|
||||
}
|
||||
|
||||
export class EasJsonReader {
|
||||
private static log?: Logger;
|
||||
|
||||
public static formatEasJsonPath(projectDir: string): string {
|
||||
return path.join(projectDir, 'eas.json');
|
||||
}
|
||||
|
||||
public static setLog(log: Logger): void {
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
constructor(private projectDir: string) {}
|
||||
|
||||
public async getBuildProfileNamesAsync(): Promise<string[]> {
|
||||
const easJson = await this.readRawAsync();
|
||||
return Object.keys(easJson?.build ?? {});
|
||||
}
|
||||
|
||||
public async getCliConfigAsync(): Promise<CliConfig | null> {
|
||||
try {
|
||||
const easJson = await this.readRawAsync();
|
||||
if (!easJson.cli) {
|
||||
return null;
|
||||
}
|
||||
const { value, error } = CliConfigSchema.validate(easJson.cli, {
|
||||
allowUnknown: false,
|
||||
convert: true,
|
||||
abortEarly: false,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(`"cli" field in eas.json is not valid [${error.toString()}]`);
|
||||
}
|
||||
return value as CliConfig;
|
||||
} catch (err: any) {
|
||||
if (err instanceof MissingEasJsonError) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async getSubmitProfileNamesAsync({ throwIfEasJsonDoesNotExist = true } = {}): Promise<
|
||||
string[]
|
||||
> {
|
||||
try {
|
||||
const easJson = await this.readRawAsync();
|
||||
return Object.keys(easJson?.submit ?? {});
|
||||
} catch (err: any) {
|
||||
if (!throwIfEasJsonDoesNotExist && err.code === 'ENOENT') {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async readBuildProfileAsync<T extends Platform>(
|
||||
platform: T,
|
||||
profileName: string
|
||||
): Promise<BuildProfile<T>> {
|
||||
const easJson = await this.readAndValidateAsync();
|
||||
this.ensureBuildProfileExists(easJson, profileName);
|
||||
const {
|
||||
android: resolvedAndroidSpecificValues,
|
||||
ios: resolvedIosSpecificValues,
|
||||
...resolvedProfile
|
||||
} = this.resolveBuildProfile(easJson, profileName);
|
||||
if (platform === Platform.ANDROID) {
|
||||
const profileWithoutDefaults = profileMerge(
|
||||
resolvedProfile,
|
||||
resolvedAndroidSpecificValues ?? {}
|
||||
);
|
||||
return profileMerge(defaults, profileWithoutDefaults) as BuildProfile<T>;
|
||||
} else if (platform === Platform.IOS) {
|
||||
const profileWithoutDefaults = profileMerge(resolvedProfile, resolvedIosSpecificValues ?? {});
|
||||
return profileMerge(defaults, profileWithoutDefaults) as BuildProfile<T>;
|
||||
} else {
|
||||
throw new Error(`Unknown platform ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async readSubmitProfileAsync<T extends Platform>(
|
||||
platform: T,
|
||||
profileNameArg?: string
|
||||
): Promise<SubmitProfile<T>> {
|
||||
let profileName = profileNameArg;
|
||||
|
||||
if (!profileName) {
|
||||
const profileNames = await this.getSubmitProfileNamesAsync({
|
||||
throwIfEasJsonDoesNotExist: false,
|
||||
});
|
||||
|
||||
if (profileNames.includes('production')) {
|
||||
profileName = 'production';
|
||||
} else if (profileNames.includes('release')) {
|
||||
profileName = 'release';
|
||||
} else {
|
||||
return getDefaultSubmitProfile(platform);
|
||||
}
|
||||
}
|
||||
|
||||
const easJson = await this.readAndValidateAsync();
|
||||
const profile = easJson?.submit?.[profileName];
|
||||
if (!profile) {
|
||||
throw new Error(`There is no submit profile named ${profileName} in eas.json`);
|
||||
}
|
||||
const platformProfile = profile[platform];
|
||||
if (platformProfile) {
|
||||
return this.evaluateFields(platform, platformProfile as SubmitProfile<T>);
|
||||
} else {
|
||||
return getDefaultSubmitProfile(platform);
|
||||
}
|
||||
}
|
||||
|
||||
public async readAndValidateAsync(): Promise<EasJson> {
|
||||
const easJson = await this.readRawAsync();
|
||||
const { value, error } = EasJsonSchema.validate(easJson, {
|
||||
allowUnknown: false,
|
||||
convert: true,
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
|
||||
}
|
||||
return value as EasJson;
|
||||
}
|
||||
|
||||
public async readRawAsync(): Promise<EasJsonPreValidation> {
|
||||
try {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(this.projectDir);
|
||||
if (!(await fs.pathExists(easJsonPath))) {
|
||||
throw new MissingEasJsonError(
|
||||
`An eas.json file could not be found at ${easJsonPath}. You must make one in order to proceed. Learn more at https://expo.fyi/eas-json`
|
||||
);
|
||||
}
|
||||
const rawEasJson = JsonFile.read(easJsonPath);
|
||||
const { value, error } = MinimalEasJsonSchema.validate(rawEasJson, { abortEarly: false });
|
||||
if (error) {
|
||||
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
|
||||
}
|
||||
return value;
|
||||
} catch (err: any) {
|
||||
if (err.code === 'EJSONPARSE') {
|
||||
err.message = `Found invalid JSON in eas.json. ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveBuildProfile(
|
||||
easJson: EasJson,
|
||||
profileName: string,
|
||||
depth: number = 0
|
||||
): RawBuildProfile {
|
||||
if (depth >= 2) {
|
||||
throw new Error(
|
||||
'Too long chain of build profile extensions, make sure "extends" keys do not make a cycle'
|
||||
);
|
||||
}
|
||||
const buildProfile = easJson.build[profileName];
|
||||
if (!buildProfile) {
|
||||
throw new Error(`There is no build profile named ${profileName} in eas.json`);
|
||||
}
|
||||
const { extends: baseProfileName, ...buildProfileRest } = buildProfile;
|
||||
if (baseProfileName) {
|
||||
return profileMerge(
|
||||
this.resolveBuildProfile(easJson, baseProfileName, depth + 1),
|
||||
buildProfileRest
|
||||
);
|
||||
} else {
|
||||
return buildProfileRest;
|
||||
}
|
||||
}
|
||||
|
||||
private ensureBuildProfileExists(easJson: EasJson, profileName: string): void {
|
||||
if (!easJson.build || !easJson.build[profileName]) {
|
||||
throw new Error(`There is no build profile named ${profileName} in eas.json`);
|
||||
}
|
||||
}
|
||||
|
||||
private evaluateFields<T extends Platform>(
|
||||
platform: T,
|
||||
profile: SubmitProfile<T>
|
||||
): SubmitProfile<T> {
|
||||
const fields =
|
||||
platform === Platform.ANDROID
|
||||
? AndroidSubmitProfileFieldsToEvaluate
|
||||
: IosSubmitProfileFieldsToEvaluate;
|
||||
const evaluatedProfile = { ...profile };
|
||||
for (const field of fields) {
|
||||
if (field in evaluatedProfile) {
|
||||
// @ts-ignore
|
||||
evaluatedProfile[field] = envString(evaluatedProfile[field], process.env);
|
||||
}
|
||||
}
|
||||
return evaluatedProfile;
|
||||
}
|
||||
}
|
||||
|
||||
export function profileMerge(base: RawBuildProfile, update: RawBuildProfile): RawBuildProfile {
|
||||
const result = {
|
||||
...base,
|
||||
...update,
|
||||
};
|
||||
if (base.env && update.env) {
|
||||
result.env = {
|
||||
...base.env,
|
||||
...update.env,
|
||||
};
|
||||
}
|
||||
if (base.android && update.android) {
|
||||
result.android = profileMerge(base.android, update.android);
|
||||
}
|
||||
if (base.ios && update.ios) {
|
||||
result.ios = profileMerge(base.ios, update.ios);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getDefaultSubmitProfile<T extends Platform>(platform: T): SubmitProfile<T> {
|
||||
const Schema =
|
||||
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema;
|
||||
return Schema.validate({}, { convert: true }).value;
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Android, Ios } from '@expo/eas-build-job';
|
||||
import Joi from 'joi';
|
||||
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack } from './EasSubmit.types';
|
||||
|
||||
const semverSchemaCheck = (value: any): any => {
|
||||
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error(`${value} is not a valid version`);
|
||||
}
|
||||
};
|
||||
|
||||
const CacheSchema = Joi.object({
|
||||
disabled: Joi.boolean(),
|
||||
key: Joi.string().max(128),
|
||||
cacheDefaultPaths: Joi.boolean(),
|
||||
customPaths: Joi.array().items(Joi.string()),
|
||||
});
|
||||
|
||||
const CommonBuildProfileSchema = Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote'),
|
||||
distribution: Joi.string().valid('store', 'internal'),
|
||||
cache: CacheSchema,
|
||||
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
developmentClient: Joi.boolean(),
|
||||
|
||||
node: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
|
||||
});
|
||||
|
||||
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
withoutCredentials: Joi.boolean(),
|
||||
|
||||
image: Joi.string().valid(...Android.builderBaseImages),
|
||||
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
autoIncrement: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('version', 'versionCode')
|
||||
),
|
||||
|
||||
artifactPath: Joi.string(),
|
||||
gradleCommand: Joi.string(),
|
||||
|
||||
buildType: Joi.string().valid('apk', 'app-bundle'),
|
||||
})
|
||||
);
|
||||
|
||||
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
|
||||
autoIncrement: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('version', 'buildNumber')
|
||||
),
|
||||
simulator: Joi.boolean(),
|
||||
|
||||
image: Joi.string().valid(...Ios.builderBaseImages),
|
||||
bundler: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
|
||||
artifactPath: Joi.string(),
|
||||
scheme: Joi.string(),
|
||||
buildConfiguration: Joi.string(),
|
||||
})
|
||||
);
|
||||
|
||||
const EasJsonBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
extends: Joi.string(),
|
||||
android: AndroidBuildProfileSchema,
|
||||
ios: IosBuildProfileSchema,
|
||||
})
|
||||
);
|
||||
|
||||
export const AndroidSubmitProfileSchema = Joi.object({
|
||||
serviceAccountKeyPath: Joi.string(),
|
||||
track: Joi.string()
|
||||
.valid(...Object.values(AndroidReleaseTrack))
|
||||
.default(AndroidReleaseTrack.internal),
|
||||
releaseStatus: Joi.string()
|
||||
.valid(...Object.values(AndroidReleaseStatus))
|
||||
.default(AndroidReleaseStatus.completed),
|
||||
changesNotSentForReview: Joi.boolean().default(false),
|
||||
applicationId: Joi.string(),
|
||||
});
|
||||
|
||||
export const IosSubmitProfileSchema = Joi.object({
|
||||
ascApiKeyPath: Joi.string(),
|
||||
ascApiKeyId: Joi.string(),
|
||||
ascApiKeyIssuerId: Joi.string(),
|
||||
appleId: Joi.string(),
|
||||
ascAppId: Joi.string(),
|
||||
appleTeamId: Joi.string(),
|
||||
sku: Joi.string(),
|
||||
language: Joi.string().default('en-US'),
|
||||
companyName: Joi.string(),
|
||||
appName: Joi.string(),
|
||||
bundleIdentifier: Joi.string(),
|
||||
});
|
||||
|
||||
const EasJsonSubmitConfigurationSchema = Joi.object({
|
||||
android: AndroidSubmitProfileSchema,
|
||||
ios: IosSubmitProfileSchema,
|
||||
});
|
||||
|
||||
export const CliConfigSchema = Joi.object({
|
||||
version: Joi.string(),
|
||||
requireCommit: Joi.boolean(),
|
||||
});
|
||||
|
||||
export const MinimalEasJsonSchema = Joi.object({
|
||||
cli: Joi.object(),
|
||||
build: Joi.object().pattern(Joi.string(), Joi.object()),
|
||||
submit: Joi.object().pattern(Joi.string(), Joi.object()),
|
||||
});
|
||||
|
||||
export const EasJsonSchema = Joi.object({
|
||||
cli: CliConfigSchema,
|
||||
build: Joi.object().pattern(Joi.string(), EasJsonBuildProfileSchema),
|
||||
submit: Joi.object().pattern(Joi.string(), EasJsonSubmitConfigurationSchema),
|
||||
});
|
||||
+50
-52
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { EasJsonReader } from '../EasJsonReader';
|
||||
import { EasJsonReader } from '../reader';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
@@ -19,18 +19,18 @@ test('minimal valid eas.json for both platforms', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'production');
|
||||
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'production');
|
||||
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
|
||||
expect({
|
||||
expect(androidProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
}).toEqual(androidProfile);
|
||||
});
|
||||
|
||||
expect({
|
||||
expect(iosProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
}).toEqual(iosProfile);
|
||||
});
|
||||
});
|
||||
|
||||
test('valid eas.json for development client builds', async () => {
|
||||
@@ -47,20 +47,20 @@ test('valid eas.json for development client builds', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'debug');
|
||||
const androidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'debug');
|
||||
expect({
|
||||
const iosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'debug');
|
||||
const androidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'debug');
|
||||
expect(androidProfile).toEqual({
|
||||
credentialsSource: 'remote',
|
||||
distribution: 'store',
|
||||
developmentClient: true,
|
||||
withoutCredentials: true,
|
||||
}).toEqual(androidProfile);
|
||||
});
|
||||
|
||||
expect({
|
||||
expect(iosProfile).toEqual({
|
||||
credentialsSource: 'remote',
|
||||
distribution: 'store',
|
||||
developmentClient: true,
|
||||
}).toEqual(iosProfile);
|
||||
});
|
||||
});
|
||||
|
||||
test('valid profile for internal distribution on Android', async () => {
|
||||
@@ -73,11 +73,11 @@ test('valid profile for internal distribution on Android', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const profile = await reader.readBuildProfileAsync(Platform.ANDROID, 'internal');
|
||||
expect({
|
||||
const profile = await reader.getBuildProfileAsync(Platform.ANDROID, 'internal');
|
||||
expect(profile).toEqual({
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
}).toEqual(profile);
|
||||
});
|
||||
});
|
||||
|
||||
test('valid profile extending other profile', async () => {
|
||||
@@ -95,18 +95,18 @@ test('valid profile extending other profile', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
expect({
|
||||
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
expect(baseProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
node: '12.0.0',
|
||||
}).toEqual(baseProfile);
|
||||
expect({
|
||||
});
|
||||
expect(extendedProfile).toEqual({
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
node: '13.0.0',
|
||||
}).toEqual(extendedProfile);
|
||||
});
|
||||
});
|
||||
|
||||
test('valid profile extending other profile with platform specific envs', async () => {
|
||||
@@ -134,33 +134,33 @@ test('valid profile extending other profile with platform specific envs', async
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
const extendedIosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'extension');
|
||||
expect({
|
||||
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
const extendedIosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'extension');
|
||||
expect(baseProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
env: {
|
||||
BASE_ENV: '1',
|
||||
PROFILE: 'base',
|
||||
},
|
||||
}).toEqual(baseProfile);
|
||||
expect({
|
||||
});
|
||||
expect(extendedAndroidProfile).toEqual({
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
env: {
|
||||
BASE_ENV: '1',
|
||||
PROFILE: 'extension:android',
|
||||
},
|
||||
}).toEqual(extendedAndroidProfile);
|
||||
expect({
|
||||
});
|
||||
expect(extendedIosProfile).toEqual({
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
env: {
|
||||
BASE_ENV: '1',
|
||||
PROFILE: 'extension',
|
||||
},
|
||||
}).toEqual(extendedIosProfile);
|
||||
});
|
||||
});
|
||||
|
||||
test('valid profile extending other profile with platform specific caching', async () => {
|
||||
@@ -188,32 +188,32 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const baseProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await reader.readBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
const extendedIosProfile = await reader.readBuildProfileAsync(Platform.IOS, 'extension');
|
||||
expect({
|
||||
const baseProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await reader.getBuildProfileAsync(Platform.ANDROID, 'extension');
|
||||
const extendedIosProfile = await reader.getBuildProfileAsync(Platform.IOS, 'extension');
|
||||
expect(baseProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
cache: {
|
||||
disabled: true,
|
||||
},
|
||||
}).toEqual(baseProfile);
|
||||
expect({
|
||||
});
|
||||
expect(extendedAndroidProfile).toEqual({
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
cache: {
|
||||
cacheDefaultPaths: false,
|
||||
customPaths: ['somefakepath'],
|
||||
},
|
||||
}).toEqual(extendedAndroidProfile);
|
||||
expect({
|
||||
});
|
||||
expect(extendedIosProfile).toEqual({
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
|
||||
cache: {
|
||||
key: 'extend-key',
|
||||
},
|
||||
}).toEqual(extendedIosProfile);
|
||||
});
|
||||
});
|
||||
|
||||
test('valid eas.json with missing profile', async () => {
|
||||
@@ -224,8 +224,8 @@ test('valid eas.json with missing profile', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'debug');
|
||||
await expect(promise).rejects.toThrowError('There is no build profile named debug in eas.json');
|
||||
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'debug');
|
||||
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: debug');
|
||||
});
|
||||
|
||||
test('invalid eas.json when using wrong buildType', async () => {
|
||||
@@ -236,7 +236,7 @@ test('invalid eas.json when using wrong buildType', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'eas.json is not valid [ValidationError: "build.production.android.buildType" must be one of [apk, app-bundle]]'
|
||||
);
|
||||
@@ -246,23 +246,21 @@ test('empty json', async () => {
|
||||
await fs.writeJson('/project/eas.json', {});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'There is no build profile named production in eas.json'
|
||||
);
|
||||
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: production');
|
||||
});
|
||||
|
||||
test('invalid semver value', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: { node: '12.0.0-alpha' },
|
||||
production: { node: 'alpha' },
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'eas.json is not valid [ValidationError: "build.production.node" failed custom validation because 12.0.0-alpha is not a valid version]'
|
||||
'eas.json is not valid [ValidationError: "build.production.node" failed custom validation because alpha is not a valid version]'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -274,7 +272,7 @@ test('invalid release channel', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const promise = reader.readBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
const promise = reader.getBuildProfileAsync(Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
|
||||
});
|
||||
|
||||
@@ -282,7 +280,7 @@ test('get profile names', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: { node: '12.0.0-alpha' },
|
||||
blah: { node: '12.0.0-alpha' },
|
||||
blah: { node: '12.0.0' },
|
||||
},
|
||||
});
|
||||
|
||||
+74
-7
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { EasJsonReader } from '../EasJsonReader';
|
||||
import { EasJsonReader } from '../reader';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
@@ -19,8 +19,8 @@ test('minimal allowed eas.json for both platforms', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'production');
|
||||
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
|
||||
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'production');
|
||||
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
|
||||
|
||||
expect(androidProfile).toEqual({
|
||||
changesNotSentForReview: false,
|
||||
@@ -46,7 +46,7 @@ test('android config with all required values', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
|
||||
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
|
||||
|
||||
expect(androidProfile).toEqual({
|
||||
serviceAccountKeyPath: './path.json',
|
||||
@@ -72,7 +72,7 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
|
||||
try {
|
||||
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
|
||||
const reader = new EasJsonReader('/project');
|
||||
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'production');
|
||||
const androidProfile = await reader.getSubmitProfileAsync(Platform.ANDROID, 'production');
|
||||
|
||||
expect(androidProfile).toEqual({
|
||||
serviceAccountKeyPath: './path.json',
|
||||
@@ -102,7 +102,7 @@ test('ios config with all required values', async () => {
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'production');
|
||||
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'production');
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
appleId: '[email protected]',
|
||||
@@ -136,7 +136,7 @@ test('ios config with ascApiKey fields set to env var', async () => {
|
||||
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
|
||||
process.env.ASC_API_KEY_ID = 'ABCD';
|
||||
const reader = new EasJsonReader('/project');
|
||||
const iosProfile = await reader.readSubmitProfileAsync(Platform.IOS, 'release');
|
||||
const iosProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'release');
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
appleId: '[email protected]',
|
||||
@@ -153,3 +153,70 @@ test('ios config with ascApiKey fields set to env var', async () => {
|
||||
process.env.ASC_API_KEY_ID = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
test('valid profile extending other profile', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
submit: {
|
||||
base: {
|
||||
ios: {
|
||||
appleId: '[email protected]',
|
||||
ascAppId: '1223423523',
|
||||
appleTeamId: 'QWERTY',
|
||||
},
|
||||
},
|
||||
extension: {
|
||||
extends: 'base',
|
||||
ios: {
|
||||
appleTeamId: 'ABCDEF',
|
||||
ascApiKeyPath: './path-ABCD.p8',
|
||||
ascApiKeyIssuerId: 'abc-123-def-456',
|
||||
ascApiKeyId: 'ABCD',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const baseProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'base');
|
||||
const extendedProfile = await reader.getSubmitProfileAsync(Platform.IOS, 'extension');
|
||||
expect(baseProfile).toEqual({
|
||||
language: 'en-US',
|
||||
appleId: '[email protected]',
|
||||
ascAppId: '1223423523',
|
||||
appleTeamId: 'QWERTY',
|
||||
});
|
||||
expect(extendedProfile).toEqual({
|
||||
language: 'en-US',
|
||||
appleId: '[email protected]',
|
||||
ascAppId: '1223423523',
|
||||
appleTeamId: 'ABCDEF',
|
||||
ascApiKeyPath: './path-ABCD.p8',
|
||||
ascApiKeyIssuerId: 'abc-123-def-456',
|
||||
ascApiKeyId: 'ABCD',
|
||||
});
|
||||
});
|
||||
|
||||
test('get profile names', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
submit: {
|
||||
production: {
|
||||
android: {
|
||||
serviceAccountKeyPath: './path.json',
|
||||
track: 'beta',
|
||||
releaseStatus: 'completed',
|
||||
},
|
||||
},
|
||||
blah: {
|
||||
android: {
|
||||
serviceAccountKeyPath: './path.json',
|
||||
track: 'internal',
|
||||
releaseStatus: 'completed',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project');
|
||||
const allProfileNames = await reader.getSubmitProfileNamesAsync();
|
||||
expect(allProfileNames.sort()).toEqual(['production', 'blah'].sort());
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { MissingParentProfileError, MissingProfileError } from '../errors';
|
||||
import { EasJson } from '../types';
|
||||
import { BuildProfileSchema } from './schema';
|
||||
import { BuildProfile, EasJsonBuildProfile } from './types';
|
||||
|
||||
type EasJsonBuildProfileResolved = Omit<EasJsonBuildProfile, 'extends'>;
|
||||
|
||||
export function resolveBuildProfile<T extends Platform>({
|
||||
easJson,
|
||||
platform,
|
||||
profileName,
|
||||
}: {
|
||||
easJson: EasJson;
|
||||
platform: T;
|
||||
profileName: string;
|
||||
}): BuildProfile<T> {
|
||||
const easJsonProfile = resolveProfile({
|
||||
easJson,
|
||||
profileName,
|
||||
});
|
||||
const { android, ios, ...base } = easJsonProfile;
|
||||
const withoutDefaults = mergeProfiles(base, easJsonProfile[platform] ?? {});
|
||||
return mergeProfiles(getDefaultProfile(platform), withoutDefaults) as BuildProfile<T>;
|
||||
}
|
||||
|
||||
function resolveProfile({
|
||||
easJson,
|
||||
profileName,
|
||||
depth = 0,
|
||||
}: {
|
||||
easJson: EasJson;
|
||||
profileName: string;
|
||||
depth?: number;
|
||||
}): EasJsonBuildProfileResolved {
|
||||
if (depth >= 2) {
|
||||
throw new Error(
|
||||
'Too long chain of profile extensions, make sure "extends" keys do not make a cycle'
|
||||
);
|
||||
}
|
||||
|
||||
const profile = easJson.build?.[profileName];
|
||||
if (!profile) {
|
||||
if (depth === 0) {
|
||||
throw new MissingProfileError(`Missing build profile in eas.json: ${profileName}`);
|
||||
} else {
|
||||
throw new MissingParentProfileError(
|
||||
`Extending non-existent build profile in eas.json: ${profileName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { extends: baseProfileName, ...rest } = profile;
|
||||
if (baseProfileName) {
|
||||
const baseProfile = resolveProfile({
|
||||
easJson,
|
||||
profileName: baseProfileName,
|
||||
depth: depth + 1,
|
||||
});
|
||||
return mergeProfiles(baseProfile, rest);
|
||||
} else {
|
||||
return rest;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeProfiles(
|
||||
base: EasJsonBuildProfileResolved,
|
||||
update: EasJsonBuildProfileResolved
|
||||
): EasJsonBuildProfileResolved {
|
||||
const result = {
|
||||
...base,
|
||||
...update,
|
||||
};
|
||||
if (base.env && update.env) {
|
||||
result.env = {
|
||||
...base.env,
|
||||
...update.env,
|
||||
};
|
||||
}
|
||||
if (base.android && update.android) {
|
||||
result.android = mergeProfiles(base.android, update.android);
|
||||
}
|
||||
if (base.ios && update.ios) {
|
||||
result.ios = mergeProfiles(base.ios, update.ios);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getDefaultProfile<T extends Platform>(platform: T): EasJsonBuildProfile {
|
||||
const defaultProfile = BuildProfileSchema.validate(
|
||||
{},
|
||||
{ allowUnknown: false, abortEarly: false, convert: true }
|
||||
).value;
|
||||
const { android, ios, ...base } = defaultProfile;
|
||||
return mergeProfiles(base, defaultProfile[platform]);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Android, Ios } from '@expo/eas-build-job';
|
||||
import Joi from 'joi';
|
||||
import semver from 'semver';
|
||||
|
||||
const CacheSchema = Joi.object({
|
||||
disabled: Joi.boolean(),
|
||||
key: Joi.string().max(128),
|
||||
cacheDefaultPaths: Joi.boolean(),
|
||||
customPaths: Joi.array().items(Joi.string()),
|
||||
});
|
||||
|
||||
const CommonBuildProfileSchema = Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
|
||||
distribution: Joi.string().valid('store', 'internal').default('store'),
|
||||
cache: CacheSchema,
|
||||
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
developmentClient: Joi.boolean(),
|
||||
|
||||
node: Joi.string().empty(null).custom(semverCheck),
|
||||
yarn: Joi.string().empty(null).custom(semverCheck),
|
||||
expoCli: Joi.string().empty(null).custom(semverCheck),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
|
||||
});
|
||||
|
||||
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote'),
|
||||
distribution: Joi.string().valid('store', 'internal'),
|
||||
withoutCredentials: Joi.boolean(),
|
||||
|
||||
image: Joi.string().valid(...Android.builderBaseImages),
|
||||
ndk: Joi.string().empty(null).custom(semverCheck),
|
||||
autoIncrement: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('version', 'versionCode')
|
||||
),
|
||||
|
||||
artifactPath: Joi.string(),
|
||||
gradleCommand: Joi.string(),
|
||||
|
||||
buildType: Joi.string().valid('apk', 'app-bundle'),
|
||||
})
|
||||
);
|
||||
|
||||
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote'),
|
||||
distribution: Joi.string().valid('store', 'internal'),
|
||||
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
|
||||
autoIncrement: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('version', 'buildNumber')
|
||||
),
|
||||
simulator: Joi.boolean(),
|
||||
|
||||
image: Joi.string().valid(...Ios.builderBaseImages),
|
||||
bundler: Joi.string().empty(null).custom(semverCheck),
|
||||
fastlane: Joi.string().empty(null).custom(semverCheck),
|
||||
cocoapods: Joi.string().empty(null).custom(semverCheck),
|
||||
|
||||
artifactPath: Joi.string(),
|
||||
scheme: Joi.string(),
|
||||
buildConfiguration: Joi.string(),
|
||||
})
|
||||
);
|
||||
|
||||
export const BuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
extends: Joi.string(),
|
||||
android: AndroidBuildProfileSchema,
|
||||
ios: IosBuildProfileSchema,
|
||||
})
|
||||
);
|
||||
|
||||
function semverCheck(value: any): any {
|
||||
if (semver.valid(value)) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error(`${value} is not a valid version`);
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,10 @@ export interface IosBuildProfile extends CommonBuildProfile {
|
||||
|
||||
export type BuildProfile<TPlatform extends Platform = Platform> = TPlatform extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: TPlatform extends Platform.IOS
|
||||
? IosBuildProfile
|
||||
: TPlatform extends Platform
|
||||
? AndroidBuildProfile | IosBuildProfile
|
||||
: never;
|
||||
: IosBuildProfile;
|
||||
|
||||
export interface EasJsonBuildProfile extends Partial<CommonBuildProfile> {
|
||||
extends?: string;
|
||||
[Platform.ANDROID]?: Partial<AndroidBuildProfile>;
|
||||
[Platform.IOS]?: Partial<IosBuildProfile>;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
export class InvalidEasJsonError extends Error {}
|
||||
|
||||
export class MissingEasJsonError extends Error {}
|
||||
|
||||
export class MissingProfileError extends Error {}
|
||||
|
||||
export class MissingParentProfileError extends Error {}
|
||||
@@ -1,20 +1,13 @@
|
||||
export { AndroidReleaseStatus, AndroidReleaseTrack, SubmitProfile } from './submit/types';
|
||||
export { getDefaultProfile as getDefaultSubmitProfile } from './submit/resolver';
|
||||
export { EasJson, ProfileType } from './types';
|
||||
export {
|
||||
AndroidSubmitProfile,
|
||||
AndroidReleaseStatus,
|
||||
AndroidReleaseTrack,
|
||||
IosSubmitProfile,
|
||||
SubmitProfile,
|
||||
} from './EasSubmit.types';
|
||||
export { EasJson } from './EasJson.types';
|
||||
export {
|
||||
AndroidBuildProfile,
|
||||
AndroidVersionAutoIncrement,
|
||||
BuildProfile,
|
||||
CredentialsSource,
|
||||
DistributionType,
|
||||
IosBuildProfile,
|
||||
IosEnterpriseProvisioning,
|
||||
IosVersionAutoIncrement,
|
||||
AndroidVersionAutoIncrement,
|
||||
} from './EasBuild.types';
|
||||
export { EasJsonReader } from './EasJsonReader';
|
||||
} from './build/types';
|
||||
export { EasJsonReader } from './reader';
|
||||
export * as errors from './errors';
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { resolveBuildProfile } from './build/resolver';
|
||||
import { BuildProfile } from './build/types';
|
||||
import { InvalidEasJsonError, MissingEasJsonError } from './errors';
|
||||
import { EasJsonSchema } from './schema';
|
||||
import { resolveSubmitProfile } from './submit/resolver';
|
||||
import { SubmitProfile } from './submit/types';
|
||||
import { EasJson } from './types';
|
||||
|
||||
export class EasJsonReader {
|
||||
private easJson: EasJson | undefined;
|
||||
|
||||
constructor(private projectDir: string) {}
|
||||
|
||||
public static formatEasJsonPath(projectDir: string): string {
|
||||
return path.join(projectDir, 'eas.json');
|
||||
}
|
||||
|
||||
public async readAsync(): Promise<EasJson> {
|
||||
if (this.easJson) {
|
||||
return this.easJson;
|
||||
}
|
||||
|
||||
try {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(this.projectDir);
|
||||
if (!(await fs.pathExists(easJsonPath))) {
|
||||
throw new MissingEasJsonError(
|
||||
`eas.json could not be found at ${easJsonPath}. Learn more at https://expo.fyi/eas-json`
|
||||
);
|
||||
}
|
||||
const contents = JsonFile.read(easJsonPath);
|
||||
const { value, error } = EasJsonSchema.validate(contents, {
|
||||
allowUnknown: false,
|
||||
abortEarly: false,
|
||||
convert: true,
|
||||
noDefaults: true,
|
||||
});
|
||||
if (error) {
|
||||
throw new InvalidEasJsonError(`eas.json is not valid [${error.toString()}]`);
|
||||
}
|
||||
this.easJson = value;
|
||||
return value;
|
||||
} catch (err: any) {
|
||||
if (err.code === 'EJSONPARSE') {
|
||||
err.message = `Found invalid JSON in eas.json. ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async getBuildProfileNamesAsync(): Promise<string[]> {
|
||||
const easJson = await this.readAsync();
|
||||
return Object.keys(easJson?.build ?? {});
|
||||
}
|
||||
|
||||
public async getBuildProfileAsync<T extends Platform>(
|
||||
platform: T,
|
||||
profileName: string
|
||||
): Promise<BuildProfile<T>> {
|
||||
const easJson = await this.readAsync();
|
||||
return resolveBuildProfile({ easJson, platform, profileName });
|
||||
}
|
||||
|
||||
public async getCliConfigAsync(): Promise<EasJson['cli'] | null> {
|
||||
try {
|
||||
const easJson = await this.readAsync();
|
||||
return easJson.cli ?? null;
|
||||
} catch (err: any) {
|
||||
if (err instanceof MissingEasJsonError) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async getSubmitProfileNamesAsync(): Promise<string[]> {
|
||||
const easJson = await this.readAsync();
|
||||
return Object.keys(easJson?.submit ?? {});
|
||||
}
|
||||
|
||||
public async getSubmitProfileAsync<T extends Platform>(
|
||||
platform: T,
|
||||
profileName: string
|
||||
): Promise<SubmitProfile<T>> {
|
||||
const easJson = await this.readAsync();
|
||||
return resolveSubmitProfile({ easJson, platform, profileName });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
import { BuildProfileSchema } from './build/schema';
|
||||
import { SubmitProfileSchema } from './submit/schema';
|
||||
|
||||
export const EasJsonSchema = Joi.object({
|
||||
cli: Joi.object({
|
||||
version: Joi.string(),
|
||||
requireCommit: Joi.boolean(),
|
||||
}),
|
||||
build: Joi.object().pattern(Joi.string(), BuildProfileSchema),
|
||||
submit: Joi.object().pattern(Joi.string(), SubmitProfileSchema),
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import envString from 'env-string';
|
||||
|
||||
import { MissingParentProfileError, MissingProfileError } from '../errors';
|
||||
import { EasJson } from '../types';
|
||||
import { AndroidSubmitProfileSchema, IosSubmitProfileSchema } from './schema';
|
||||
import {
|
||||
AndroidSubmitProfileFieldsToEvaluate,
|
||||
IosSubmitProfileFieldsToEvaluate,
|
||||
SubmitProfile,
|
||||
} from './types';
|
||||
|
||||
export function resolveSubmitProfile<T extends Platform>({
|
||||
easJson,
|
||||
platform,
|
||||
profileName,
|
||||
}: {
|
||||
easJson: EasJson;
|
||||
platform: T;
|
||||
profileName: string;
|
||||
}): SubmitProfile<T> {
|
||||
const submitProfile = resolveProfile({
|
||||
easJson,
|
||||
platform,
|
||||
profileName,
|
||||
});
|
||||
const unevaluatedProfile = mergeProfiles(getDefaultProfile(platform), submitProfile);
|
||||
return evaluateFields(platform, unevaluatedProfile);
|
||||
}
|
||||
|
||||
function resolveProfile<T extends Platform>({
|
||||
easJson,
|
||||
profileName,
|
||||
depth = 0,
|
||||
platform,
|
||||
}: {
|
||||
platform: T;
|
||||
easJson: EasJson;
|
||||
profileName: string;
|
||||
depth?: number;
|
||||
}): SubmitProfile<T> | undefined {
|
||||
if (depth >= 2) {
|
||||
throw new Error(
|
||||
'Too long chain of profile extensions, make sure "extends" keys do not make a cycle'
|
||||
);
|
||||
}
|
||||
|
||||
const profile = easJson.submit?.[profileName];
|
||||
if (!profile) {
|
||||
if (depth === 0) {
|
||||
throw new MissingProfileError(`Missing submit profile in eas.json: ${profileName}`);
|
||||
} else {
|
||||
throw new MissingParentProfileError(
|
||||
`Extending non-existent submit profile in eas.json: ${profileName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { extends: baseProfileName, ...rest } = profile;
|
||||
const platformProfile = rest[platform] as SubmitProfile<T> | undefined;
|
||||
if (baseProfileName) {
|
||||
const baseProfile = resolveProfile({
|
||||
easJson,
|
||||
platform,
|
||||
profileName: baseProfileName,
|
||||
depth: depth + 1,
|
||||
});
|
||||
return mergeProfiles(baseProfile, platformProfile);
|
||||
} else {
|
||||
return platformProfile;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeProfiles<T extends Platform>(
|
||||
base: SubmitProfile<T>,
|
||||
update?: SubmitProfile<T>
|
||||
): SubmitProfile<T>;
|
||||
function mergeProfiles<T extends Platform>(
|
||||
base?: SubmitProfile<T>,
|
||||
update?: SubmitProfile<T>
|
||||
): SubmitProfile<T> | undefined;
|
||||
function mergeProfiles<T extends Platform>(
|
||||
base?: SubmitProfile<T>,
|
||||
update?: SubmitProfile<T>
|
||||
): SubmitProfile<T> | undefined {
|
||||
if (!update) {
|
||||
return base;
|
||||
}
|
||||
return { ...base, ...update };
|
||||
}
|
||||
|
||||
export function getDefaultProfile<T extends Platform>(platform: T): SubmitProfile<T> {
|
||||
const Schema =
|
||||
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema;
|
||||
return Schema.validate({}, { allowUnknown: false, abortEarly: false, convert: true }).value;
|
||||
}
|
||||
|
||||
function evaluateFields<T extends Platform>(
|
||||
platform: T,
|
||||
profile: SubmitProfile<T>
|
||||
): SubmitProfile<T> {
|
||||
const fields =
|
||||
platform === Platform.ANDROID
|
||||
? AndroidSubmitProfileFieldsToEvaluate
|
||||
: IosSubmitProfileFieldsToEvaluate;
|
||||
const evaluatedProfile = { ...profile };
|
||||
for (const field of fields) {
|
||||
if (field in evaluatedProfile) {
|
||||
// @ts-ignore
|
||||
evaluatedProfile[field] = envString(evaluatedProfile[field], process.env);
|
||||
}
|
||||
}
|
||||
return evaluatedProfile;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack } from './types';
|
||||
|
||||
export const AndroidSubmitProfileSchema = Joi.object({
|
||||
serviceAccountKeyPath: Joi.string(),
|
||||
track: Joi.string()
|
||||
.valid(...Object.values(AndroidReleaseTrack))
|
||||
.default(AndroidReleaseTrack.internal),
|
||||
releaseStatus: Joi.string()
|
||||
.valid(...Object.values(AndroidReleaseStatus))
|
||||
.default(AndroidReleaseStatus.completed),
|
||||
changesNotSentForReview: Joi.boolean().default(false),
|
||||
applicationId: Joi.string(),
|
||||
});
|
||||
|
||||
export const IosSubmitProfileSchema = Joi.object({
|
||||
ascApiKeyPath: Joi.string(),
|
||||
ascApiKeyId: Joi.string(),
|
||||
ascApiKeyIssuerId: Joi.string(),
|
||||
appleId: Joi.string(),
|
||||
ascAppId: Joi.string(),
|
||||
appleTeamId: Joi.string(),
|
||||
sku: Joi.string(),
|
||||
language: Joi.string().default('en-US'),
|
||||
companyName: Joi.string(),
|
||||
appName: Joi.string(),
|
||||
bundleIdentifier: Joi.string(),
|
||||
});
|
||||
|
||||
export const SubmitProfileSchema = Joi.object({
|
||||
extends: Joi.string(),
|
||||
android: AndroidSubmitProfileSchema,
|
||||
ios: IosSubmitProfileSchema,
|
||||
});
|
||||
@@ -47,10 +47,10 @@ export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [
|
||||
];
|
||||
|
||||
export type SubmitProfile<TPlatform extends Platform = Platform> =
|
||||
TPlatform extends Platform.ANDROID
|
||||
? AndroidSubmitProfile
|
||||
: TPlatform extends Platform.IOS
|
||||
? IosSubmitProfile
|
||||
: TPlatform extends Platform
|
||||
? AndroidSubmitProfile | IosSubmitProfile
|
||||
: never;
|
||||
TPlatform extends Platform.ANDROID ? AndroidSubmitProfile : IosSubmitProfile;
|
||||
|
||||
export interface EasJsonSubmitProfile {
|
||||
extends?: string;
|
||||
[Platform.ANDROID]?: AndroidSubmitProfile;
|
||||
[Platform.IOS]?: IosSubmitProfile;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { EasJsonBuildProfile } from './build/types';
|
||||
import { EasJsonSubmitProfile } from './submit/types';
|
||||
|
||||
export type ProfileType = 'build' | 'submit';
|
||||
export type EasJsonProfile<T extends ProfileType> = T extends 'build'
|
||||
? EasJsonBuildProfile
|
||||
: EasJsonSubmitProfile;
|
||||
|
||||
export enum CredentialsSource {
|
||||
LOCAL = 'local',
|
||||
REMOTE = 'remote',
|
||||
}
|
||||
|
||||
export interface EasJson {
|
||||
cli?: {
|
||||
version?: string;
|
||||
requireCommit?: boolean;
|
||||
};
|
||||
build?: { [profileName: string]: EasJsonBuildProfile };
|
||||
submit?: { [profileName: string]: EasJsonSubmitProfile };
|
||||
}
|
||||
@@ -28,6 +28,6 @@
|
||||
"@types/node": "16.7.8",
|
||||
"@types/semver": "7.3.8",
|
||||
"ts-node": "10.2.1",
|
||||
"typescript": "4.4.2"
|
||||
"typescript": "4.5.2"
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,10 @@ function addEntry(tokens: markdown.Tokens, entry: Entry): void {
|
||||
}
|
||||
|
||||
function formatEntry({ message, prNumber, author }: Entry): string {
|
||||
const messageWithDot = message.endsWith('.') ? message : `${message}.`;
|
||||
const capitalizedMessage = `${message[0].toUpperCase()}${message.slice(1)}`;
|
||||
const messageWithDot = capitalizedMessage.endsWith('.')
|
||||
? capitalizedMessage
|
||||
: `${capitalizedMessage}.`;
|
||||
return `${messageWithDot} ([#${prNumber}](https://github.com/expo/eas-cli/pull/${prNumber}) by [@${author}](https://github.com/${author}))`;
|
||||
}
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -176,10 +176,10 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
typescript@4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
|
||||
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
|
||||
typescript@4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
|
||||
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
||||
@@ -848,14 +848,15 @@
|
||||
mv "~2"
|
||||
safe-json-stringify "~1"
|
||||
|
||||
"@expo/[email protected].3":
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.0.3.tgz#8680dea2709c70f3a9c9e77ca6057335758341a8"
|
||||
integrity sha512-vJ+4YOXxAjznpIG0WQHNsbeeJQXS/ZxzVPHhOO3le8Lkl+WJ2eH7XypulPRalBI4E0WLFtzDzLIdnqYJyjzvaw==
|
||||
"@expo/[email protected].11":
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.0.11.tgz#2c11c63ec0d3f7a56d6cb45c8340a1546f3cfc5c"
|
||||
integrity sha512-hoQq+B1NlIqWeSLKbQQ19g2vzqKhXh2glRwRFCGvqKT2/UaweFU5w7jetNPOBlVIV1+TpQmhLNbTYako6Z3vnQ==
|
||||
dependencies:
|
||||
"@expo/config-types" "^43.0.0"
|
||||
"@expo/json-file" "8.2.33"
|
||||
"@expo/plist" "0.0.15"
|
||||
"@expo/config-types" "^43.0.1"
|
||||
"@expo/json-file" "8.2.34"
|
||||
"@expo/plist" "0.0.16"
|
||||
"@expo/sdk-runtime-versions" "^1.0.0"
|
||||
"@react-native/normalize-color" "^2.0.0"
|
||||
chalk "^4.1.2"
|
||||
debug "^4.3.1"
|
||||
@@ -869,20 +870,20 @@
|
||||
xcode "^3.0.1"
|
||||
xml2js "0.4.23"
|
||||
|
||||
"@expo/config-types@^43.0.0":
|
||||
version "43.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-43.0.0.tgz#e283a4a1b72a655a6a2a745bd277caef58c8edb0"
|
||||
integrity sha512-GBbEtLdGX40DJ1wTixWtOHseupA5xWclV57KNZ58ebauWAOyK3Fyni7GD2OCOzxTIFi1nSjqgO9JlMp3ayP4Tw==
|
||||
"@expo/config-types@^43.0.1":
|
||||
version "43.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-43.0.1.tgz#3e047dccb371741a540980eaff26fb0c95039c30"
|
||||
integrity sha512-EtllpCGDdB/UdwAIs5YXJwBLpbFQNdlLLrxIvoILA9cXrpQMWkeDCT9lQPJzFRMFcLUaMuGvkzX2tR4tx5EQFQ==
|
||||
|
||||
"@expo/[email protected].3":
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.3.tgz#48e5d453ae00ca090ea47799f4a9f352d5136a89"
|
||||
integrity sha512-Qo2OVDfKbCyLiG3g2vqrPQXBBGRB2I3ADIB/8nZ3Ypy7QhqOA1L7WYhTQQr3T1zknHTMzy7u61ArHweiAnEEpw==
|
||||
"@expo/[email protected].11":
|
||||
version "6.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.11.tgz#a00815ac29c13dfb069441dff6f0b56da69baa2f"
|
||||
integrity sha512-ohmm/fjpqL3mk1Tj9BqmeoFuGNQxfMm2cQs0JHqBCXTjBUtprrH1j6wHdKR2ctNWQxpOlStPkC6CgPv+bd1+GA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "~7.10.4"
|
||||
"@expo/config-plugins" "4.0.3"
|
||||
"@expo/config-types" "^43.0.0"
|
||||
"@expo/json-file" "8.2.33"
|
||||
"@expo/config-plugins" "4.0.11"
|
||||
"@expo/config-types" "^43.0.1"
|
||||
"@expo/json-file" "8.2.34"
|
||||
getenv "^1.0.0"
|
||||
glob "7.1.6"
|
||||
require-from-string "^2.0.2"
|
||||
@@ -891,18 +892,18 @@
|
||||
slugify "^1.3.4"
|
||||
sucrase "^3.20.0"
|
||||
|
||||
"@expo/[email protected].57":
|
||||
version "0.2.57"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.57.tgz#260a686a919e3b311975a850d36d381643433e1e"
|
||||
integrity sha512-q+u/2pCx60onMEHaQxstEP88H6vEgcFhSRY5IflErFtOpgcN9gA6ZemBRnxYzC9OS8T1AF6zJqzdeIjMANjPrw==
|
||||
"@expo/[email protected].61":
|
||||
version "0.2.61"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.61.tgz#85aae117f285e05f7d17c3a759fad73b6a503455"
|
||||
integrity sha512-8lv6FInuv/yV0dYSI5QRxpTXJzQizyGnAORIHFeaAdUPTkjjsGSEuqY8ynQV0pXxLcJ79DLQqxZ8QDjglevWpA==
|
||||
dependencies:
|
||||
joi "^17.4.2"
|
||||
semver "^7.3.5"
|
||||
|
||||
"@expo/[email protected]7":
|
||||
version "0.3.17"
|
||||
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.17.tgz#75e2606749ffa1284de570245f668503e4a06c3f"
|
||||
integrity sha512-zaOj24JK5F+pPGpULP8D9aAbApcR7ixV2GjT43YzZqjHuyxQ5knI+EsbmZ1TNnA1vNJJKMYrigB5+chcUGwkpw==
|
||||
"@expo/[email protected]8":
|
||||
version "0.3.18"
|
||||
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.18.tgz#36a41ab2925974ac5707e02cb0d3694349140497"
|
||||
integrity sha512-77/ub2aGuf7SYfaFhvCHE54Hs/jRuU5j+pemS5seLfVHNwHbJSse91TMhsTLLNz3GwwqTxFVe3KMycSccJ73nA==
|
||||
dependencies:
|
||||
"@expo/spawn-async" "1.5.0"
|
||||
chalk "^4.0.0"
|
||||
@@ -925,6 +926,15 @@
|
||||
json5 "^1.0.1"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "8.2.34"
|
||||
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.34.tgz#2f24e90a677195f7a81e167115460eb2902c3130"
|
||||
integrity sha512-ZxtBodAZGxdLtgKzmsC+8ViUxt1mhFW642Clu2OuG3f6PAyAFsU/SqEGag9wKFaD3x3Wt8VhL+3y5fMJmUFgPw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "~7.10.4"
|
||||
json5 "^1.0.1"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.24.0-expo"
|
||||
resolved "https://registry.yarnpkg.com/@expo/oclif-dev-cli/-/oclif-dev-cli-1.24.0-expo.tgz#4e1f766e79b4d99e3da29318a577952793b143bf"
|
||||
@@ -945,35 +955,26 @@
|
||||
qqjs "^0.3.10"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@expo/[email protected].4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@expo/pkcs12/-/pkcs12-0.0.4.tgz#d4ba48bfcb5dcae979e96644a0ca157c0d70f075"
|
||||
integrity sha512-dYNecU+r4RIy8dPzu1SCUi+EqPhfSic6ODH6IkKLCf9m7unxEO0SvhU+rJqlgpoQ49PThOmk4EFX2KCClyrKnw==
|
||||
"@expo/[email protected].6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@expo/pkcs12/-/pkcs12-0.0.6.tgz#ef9e4691bfced9318d4a3736260ced27401fc752"
|
||||
integrity sha512-glYvPd2fv9tAQbXWprhnUYmKGmOtFJzGxhBef90bkZL8N4yNhcF1T/+l1Nrtzrf/ZSAjCnek4oXezpwOUYBnWw==
|
||||
dependencies:
|
||||
node-forge "^0.10.0"
|
||||
|
||||
"@expo/[email protected]4":
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.14.tgz#a756903bd28aabe0a961222df2e7858a39a218c9"
|
||||
integrity sha512-bb4Ua1M/OdNgS8KiGdSDUjZ/bbPfv3xdPY/lz8Ctp/adlj/QgB8xA7tVPeqSSfJPZqFRwU0qLCnRhpUOnP51VQ==
|
||||
"@expo/[email protected]6":
|
||||
version "0.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.16.tgz#c7d294a774196cfa1d8caf89a75ba923f5d1d53c"
|
||||
integrity sha512-yQMt2CTm2VNLiHv2/EqHYrHvfflI2mFJd+DZIQQy569hvpZle0CxMPGH1p2KatbrO8htxJNvwrlR/4TIwhQJ5w==
|
||||
dependencies:
|
||||
"@xmldom/xmldom" "~0.7.0"
|
||||
base64-js "^1.2.3"
|
||||
xmlbuilder "^14.0.0"
|
||||
|
||||
"@expo/pl[email protected].15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.15.tgz#41ef37b7bbe6b81c48bf4a5c359661c766bb9e90"
|
||||
integrity sha512-LDxiS0KNZAGJu4fIJhbEKczmb+zeftl1NU0LE0tj0mozoMI5HSKdMUchgvnBm35bwBl8ekKkAfJJ0ONxljWQjQ==
|
||||
dependencies:
|
||||
"@xmldom/xmldom" "~0.7.0"
|
||||
base64-js "^1.2.3"
|
||||
xmlbuilder "^14.0.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/plugin-warn-if-update-available/-/plugin-warn-if-update-available-1.7.0.tgz#d47fb15c9b0c8af91b20c9472e5f848972b472d8"
|
||||
integrity sha512-cOEVg43RusKOY/14SFmN+HCZllR+UEAr6Nv4kTI80vRpclSqn4WQaAqgSjZx5+i2cdVDfv9FeQwLdDwcmbkIIw==
|
||||
"@expo/pl[email protected].1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@expo/plugin-warn-if-update-available/-/plugin-warn-if-update-available-1.7.1.tgz#26e5dbf685801beb1e732dfa663944d280116e98"
|
||||
integrity sha512-Fo4s9EDliMTBnfaoHOXBvHLQxrHQq34RqmfzZ3qQUPEOCT0hpElEcSXa2qOM0K0WPJzhciMGIEk2ZWhZeTVWxg==
|
||||
dependencies:
|
||||
"@oclif/command" "^1.6.0"
|
||||
"@oclif/config" "^1.12.8"
|
||||
@@ -985,30 +986,31 @@
|
||||
lodash.template "^4.4.0"
|
||||
semver "^7.3.2"
|
||||
|
||||
"@expo/[email protected].3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-3.0.3.tgz#448f48aab2a28af6b12dc1a95696dbd08f5668f3"
|
||||
integrity sha512-7r/Wfzs2QNm8RvnbT1LFZ+Xkb3vrcLVGwkRt1BHPxmRFwq4SNwhkMyLPopQEjUhJU3AqLn6EKItS39+9c2f8vg==
|
||||
"@expo/[email protected].11":
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-3.0.11.tgz#362d647b695bc377d8a5dc95788b2b3e00754f5a"
|
||||
integrity sha512-RVtbTVgHGh2JSCng9xuvJAbvBW3pAWUvJLAbEkNqV/+mb2uPjDumXtyxcUfHSNX0Yjbtxw6LG61v6SNzGkfWlA==
|
||||
dependencies:
|
||||
"@expo/config" "6.0.3"
|
||||
"@expo/config-plugins" "4.0.3"
|
||||
"@expo/config-types" "^43.0.0"
|
||||
"@expo/image-utils" "0.3.17"
|
||||
"@expo/json-file" "8.2.33"
|
||||
"@expo/config" "6.0.11"
|
||||
"@expo/config-plugins" "4.0.11"
|
||||
"@expo/config-types" "^43.0.1"
|
||||
"@expo/image-utils" "0.3.18"
|
||||
"@expo/json-file" "8.2.34"
|
||||
debug "^4.3.1"
|
||||
fs-extra "^9.0.0"
|
||||
resolve-from "^5.0.0"
|
||||
semver "7.3.2"
|
||||
xml2js "0.4.23"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/results/-/results-1.0.0.tgz#fd4b22f936ceafce23b04799f54b87fe2a9e18d1"
|
||||
integrity sha512-qECzzXX5oJot3m2Gu9pfRDz50USdBieQVwYAzeAtQRUTD3PVeTK1tlRUoDcrK8PSruDLuVYdKkLebX4w/o55VA==
|
||||
|
||||
"@expo/[email protected].0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.0.tgz#90c6ef615fbf34473180aa439e3daac7b5af9eed"
|
||||
integrity sha512-XUxJi2xUEi3RNdKc78emywcl8jQ+iGzpBq0gC1Gpu2/3gAuIxIfp/vGWp1vWo58cZtabhbQx/AMmbOfsHK35Qw==
|
||||
"@expo/[email protected].1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz#6aa575f346833eb6290282118766d4919c808c6a"
|
||||
integrity sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==
|
||||
dependencies:
|
||||
"@expo/bunyan" "^4.0.0"
|
||||
"@segment/loosely-validate-event" "^2.0.0"
|
||||
@@ -1018,7 +1020,7 @@
|
||||
remove-trailing-slash "^0.1.0"
|
||||
uuid "^8.3.2"
|
||||
|
||||
"@expo/[email protected]":
|
||||
"@expo/sdk-runtime-versions@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c"
|
||||
integrity sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==
|
||||
@@ -2383,7 +2385,19 @@
|
||||
node-gyp "^7.1.0"
|
||||
read-package-json-fast "^2.0.1"
|
||||
|
||||
"@oclif/[email protected].0", "@oclif/command@^1.5.13", "@oclif/command@^1.5.20", "@oclif/command@^1.6.0":
|
||||
"@oclif/[email protected].3", "@oclif/command@^1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.8.3.tgz#1f8bc2c4ecf94b6659a5134d95da179e1dffad9e"
|
||||
integrity sha512-OGjrhdVgTT2TAAj/2RrdXjwxaDoTm16c2LfAzrta1xIFe6/XhgQIYDmeRN/RptQoZQBX8e9Vv2JoQq+TbghJmw==
|
||||
dependencies:
|
||||
"@oclif/config" "^1.15.1"
|
||||
"@oclif/errors" "^1.3.5"
|
||||
"@oclif/parser" "^3.8.5"
|
||||
"@oclif/plugin-help" "^3.2.4"
|
||||
debug "^4.1.1"
|
||||
semver "^7.3.2"
|
||||
|
||||
"@oclif/command@^1.5.13", "@oclif/command@^1.5.20", "@oclif/command@^1.6.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.8.0.tgz#c1a499b10d26e9d1a611190a81005589accbb339"
|
||||
integrity sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw==
|
||||
@@ -2395,7 +2409,19 @@
|
||||
debug "^4.1.1"
|
||||
semver "^7.3.2"
|
||||
|
||||
"@oclif/[email protected].0", "@oclif/config@^1.12.12", "@oclif/config@^1.12.8", "@oclif/config@^1.15.1":
|
||||
"@oclif/[email protected].1", "@oclif/config@^1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.17.1.tgz#383515f6715b91d8df5db8108214e93bb46e86ca"
|
||||
integrity sha512-UqV5qsN2np96TNlJspSNlRl7CpFmxYSrB0iLe3XV9NDkbFEE5prGP++h6w6xOR/FL3QV7BoqrbwGuJdJdFbidw==
|
||||
dependencies:
|
||||
"@oclif/errors" "^1.3.3"
|
||||
"@oclif/parser" "^3.8.6"
|
||||
debug "^4.1.1"
|
||||
globby "^11.0.1"
|
||||
is-wsl "^2.1.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@oclif/config@^1.12.12", "@oclif/config@^1.12.8", "@oclif/config@^1.15.1":
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.17.0.tgz#ba8639118633102a7e481760c50054623d09fcab"
|
||||
integrity sha512-Lmfuf6ubjQ4ifC/9bz1fSCHc6F6E653oyaRXxg+lgT4+bYf9bk+nqrUpAbrXyABkCqgIBiFr3J4zR/kiFdE1PA==
|
||||
@@ -2407,7 +2433,7 @@
|
||||
is-wsl "^2.1.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@oclif/[email protected]":
|
||||
"@oclif/[email protected]", "@oclif/errors@^1.3.5":
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.5.tgz#a1e9694dbeccab10fe2fe15acb7113991bed636c"
|
||||
integrity sha512-OivucXPH/eLLlOT7FkCMoZXiaVYf8I/w1eTAM1+gKzfhALwWTusxEx7wBmW0uzvkSg/9ovWLycPaBgJbM3LOCQ==
|
||||
@@ -2444,17 +2470,27 @@
|
||||
chalk "^2.4.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@oclif/p[email protected]":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.2.3.tgz#cd24010e7eb326782843d3aa6d6b5a4affebb2c3"
|
||||
integrity sha512-l2Pd0lbOMq4u/7xsl9hqISFqyR9gWEz/8+05xmrXFr67jXyS6EUCQB+mFBa0wepltrmJu0sAFg9AvA2mLaMMqQ==
|
||||
"@oclif/parser@^3.8.5", "@oclif/parser@^3.8.6":
|
||||
version "3.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.6.tgz#d5a108af9c708a051cc6b1d27d47359d75f41236"
|
||||
integrity sha512-tXb0NKgSgNxmf6baN6naK+CCwOueaFk93FG9u202U7mTBHUKsioOUlw1SG/iPi9aJM3WE4pHLXmty59pci0OEw==
|
||||
dependencies:
|
||||
"@oclif/command" "^1.5.20"
|
||||
"@oclif/config" "^1.15.1"
|
||||
"@oclif/errors" "^1.2.2"
|
||||
"@oclif/linewrap" "^1.0.0"
|
||||
chalk "^4.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@oclif/[email protected]", "@oclif/plugin-help@^3.2.4":
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.2.5.tgz#dab0f5e655971b2c49dd80ee472f97b6e69a1297"
|
||||
integrity sha512-fjkZTstvacCPicF2oaa3Lc+Yw3ocKEaW6x6O7doVqMLuoMUX6wBOQ+f1a3VFzO1fErqNeFPDlUlVUhwq9yMzQg==
|
||||
dependencies:
|
||||
"@oclif/command" "^1.8.3"
|
||||
"@oclif/config" "^1.17.1"
|
||||
"@oclif/errors" "^1.3.5"
|
||||
chalk "^4.1.0"
|
||||
indent-string "^4.0.0"
|
||||
lodash.template "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
widest-line "^3.1.0"
|
||||
@@ -7543,7 +7579,7 @@ lodash.without@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
|
||||
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
|
||||
|
||||
[email protected], lodash@^4.7.0:
|
||||
[email protected], lodash@^4.17.21, lodash@^4.7.0:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -10460,10 +10496,10 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript@4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
|
||||
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
|
||||
typescript@4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
|
||||
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.22"
|
||||
|
||||
Reference in new issue
Block a user