mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d92a2ae3f4 | ||
|
|
b7dea35da4 | ||
|
|
5a5f11752d | ||
|
|
b6c300b5c2 | ||
|
|
0117dbfa5f | ||
|
|
c106f8a1a4 | ||
|
|
8b9741a70c | ||
|
|
271b85fd74 | ||
|
|
21a481790b | ||
|
|
ab5d9237f1 | ||
|
|
756853f2f3 | ||
|
|
5eb80d15f6 | ||
|
|
a98043b4ad | ||
|
|
d0818932e7 | ||
|
|
23b60ed385 | ||
|
|
5d2811bf21 | ||
|
|
7b202f2aa7 | ||
|
|
fa6e9c1735 | ||
|
|
c3300036d8 | ||
|
|
5efcdb2940 | ||
|
|
8113450ea4 | ||
|
|
d10b0c3c65 | ||
|
|
e8de0e8712 | ||
|
|
e1da1099da | ||
|
|
264aa8fefa | ||
|
|
941d0294b7 | ||
|
|
366f4e8914 | ||
|
|
1460c3c5a2 | ||
|
|
6271472827 | ||
|
|
3bc4a054ae | ||
|
|
dd85d559d7 | ||
|
|
ab10b082e5 | ||
|
|
60b60066f9 | ||
|
|
9e9e517698 | ||
|
|
020c0f2b98 | ||
|
|
dd74ebdc51 | ||
|
|
ddf7381efb | ||
|
|
3b67b912e4 | ||
|
|
37d7fa4510 | ||
|
|
096601473a | ||
|
|
2e25ed81ca | ||
|
|
4e11ea1d77 | ||
|
|
d9d470426b | ||
|
|
119b4ad8b2 | ||
|
|
ae1e9f000d |
No files matched your search
@@ -1,3 +1,9 @@
|
||||
<!-- Thanks for contributing to _EAS CLI_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->
|
||||
|
||||
# Checklist
|
||||
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary.
|
||||
|
||||
# Why
|
||||
|
||||
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
|
||||
|
||||
+53
-1
@@ -8,13 +8,65 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Print common error messages when builds fail. ([#272](https://github.com/expo/eas-cli/pull/272) by [@dsokal](https://github.com/dsokal))
|
||||
- Commit automatically if `EAS_BUILD_AUTOCOMMIT` is set. ([#271](https://github.com/expo/eas-cli/pull/271) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Allow for installing custom `bundler` version on EAS Build. ([#277](https://github.com/expo/eas-cli/pull/277) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for managing environment secrets. ([#275](https://github.com/expo/eas-cli/pull/275) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas submit` local archive prompt for `.aab` files when submitting for iOS. ([#273](https://github.com/expo/eas-cli/pull/273) by [@barthap](https://github.com/barthap))
|
||||
- Verify whether "name" field in app.json contains any alphanumeric characters. ([#280](https://github.com/expo/eas-cli/pull/280) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Detect dependency cycles in eas.json build profiles. ([#283](https://github.com/expo/eas-cli/pull/283) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.6.0](https://github.com/expo/eas-cli/releases/tag/v0.6.0) - 2021-03-09
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Generic iOS projects: build release builds by default. ([#266](https://github.com/expo/eas-cli/pull/266) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Log the size of the archived project when uploading. ([#264](https://github.com/expo/eas-cli/pull/264) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add more build metadata (release channel, build profile name, git commit hash). ([#265](https://github.com/expo/eas-cli/pull/265) by [@dsokal](https://github.com/dsokal))
|
||||
- Display App Store link after successful submission. ([#144](https://github.com/expo/eas-cli/pull/144) by [@barthap](https://github.com/barthap))
|
||||
- Add `experimental.disableIosBundleIdentifierValidation` flag to eas.json. ([#263](https://github.com/expo/eas-cli/pull/263) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support internal distribution in non-interactive builds. ([#269](https://github.com/expo/eas-cli/pull/269) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Print Apple Team ID in the output of `device:list` when the team name is unknown. ([#268](https://github.com/expo/eas-cli/pull/268) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.5.0](https://github.com/expo/eas-cli/releases/tag/v0.5.0) - 2021-03-02
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for build cache. ([#247](https://github.com/expo/eas-cli/pull/247) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Enable internal distribution support for self-managed credentials. ([#256](https://github.com/expo/eas-cli/pull/256) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `@expo/eas-build-job` from `0.2.12` to `0.2.13`. ([#245](https://github.com/expo/eas-cli/pull/245) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.4.3](https://github.com/expo/eas-cli/releases/tag/v0.4.3) - 2021-02-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for iOS simulator builds. ([#240](https://github.com/expo/eas-cli/pull/240) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use fixed version of `@expo/eas-json`. ([#243](https://github.com/expo/eas-cli/pull/243) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.4.2](https://github.com/expo/eas-cli/releases/tag/v0.4.1) - 2021-02-18
|
||||
## [0.4.2](https://github.com/expo/eas-cli/releases/tag/v0.4.2) - 2021-02-18
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.4.3"
|
||||
"version": "0.7.0"
|
||||
}
|
||||
+76
-28
@@ -7,27 +7,28 @@ EAS command line tool
|
||||
[](https://npmjs.org/package/eas-cli)
|
||||
[](https://github.com/expo/eas-cli/blob/main/package.json)
|
||||
|
||||
<!-- toc -->
|
||||
* [eas-cli](#eas-cli)
|
||||
* [Installation](#installation)
|
||||
* [Usage](#usage)
|
||||
* [Commands](#commands)
|
||||
<!-- tocstop -->
|
||||
|
||||
# Installation
|
||||
|
||||
```sh
|
||||
npm install -g eas-cli
|
||||
# or
|
||||
yarn global add eas-cli
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
<!-- usage -->
|
||||
```sh-session
|
||||
$ npm install -g eas-cli
|
||||
$ eas COMMAND
|
||||
running command...
|
||||
$ eas (-v|--version|version)
|
||||
eas-cli/0.4.3 darwin-x64 node-v14.15.4
|
||||
$ eas --help [COMMAND]
|
||||
USAGE
|
||||
$ eas COMMAND
|
||||
...
|
||||
```sh
|
||||
eas COMMAND
|
||||
# runs the command
|
||||
eas (-v|--version|version)
|
||||
# prints the version
|
||||
eas --help COMMAND
|
||||
# outputs help for specific command
|
||||
```
|
||||
<!-- usagestop -->
|
||||
|
||||
# Commands
|
||||
|
||||
@@ -35,7 +36,9 @@ USAGE
|
||||
* [`eas account:login`](#eas-accountlogin)
|
||||
* [`eas account:logout`](#eas-accountlogout)
|
||||
* [`eas account:view`](#eas-accountview)
|
||||
* [`eas build`](#eas-build)
|
||||
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
@@ -43,6 +46,7 @@ USAGE
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas secrets:list`](#eas-secretslist)
|
||||
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
|
||||
|
||||
## `eas account:login`
|
||||
@@ -57,7 +61,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/account/login.ts)_
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -71,7 +75,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/account/logout.ts)_
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -85,7 +89,26 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/account/view.ts)_
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/account/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
Start a build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
--non-interactive Run command in --non-interactive mode
|
||||
--profile=profile [default: release] Name of the build profile from eas.json
|
||||
--skip-credentials-check Skip validation of build credentials
|
||||
--skip-project-configuration Skip project configuration
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -96,7 +119,22 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/build/cancel.ts)_
|
||||
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
Configure the project to support EAS Build.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:configure
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -112,7 +150,7 @@ OPTIONS
|
||||
--status=(in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/build/list.ts)_
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -123,7 +161,7 @@ USAGE
|
||||
$ eas build:view [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/build/view.ts)_
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -134,7 +172,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/credentials.ts)_
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -145,7 +183,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/device/create.ts)_
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -159,7 +197,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/device/list.ts)_
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -170,7 +208,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/device/view.ts)_
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -189,6 +227,17 @@ OPTIONS
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
|
||||
## `eas secrets:list`
|
||||
|
||||
Lists environment secrets available for your current app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secrets:list
|
||||
```
|
||||
|
||||
_See code: [build/commands/secrets/list.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/secrets/list.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
|
||||
Submits build artifact to app store
|
||||
@@ -213,8 +262,7 @@ OPTIONS
|
||||
|
||||
iOS specific options=asc-app-id App Store Connect unique application Apple ID
|
||||
number. Providing this param results in skipping
|
||||
app creation step. Learn more:
|
||||
https://expo.fyi/asc-app-id
|
||||
app creation step. Learn more.
|
||||
|
||||
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
|
||||
expo.ios.bundleIdentifier from app config)
|
||||
@@ -267,5 +315,5 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.4.3/build/commands/submit.ts)_
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.7.0/build/commands/submit.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -3,6 +3,7 @@ schema: 'https://staging.exp.host/--/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.4.3",
|
||||
"version": "0.7.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -10,9 +10,9 @@
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.17",
|
||||
"@expo/config": "~3.3.19",
|
||||
"@expo/config-plugins": "1.0.19-alpha.0",
|
||||
"@expo/eas-build-job": "0.2.12",
|
||||
"@expo/eas-json": "^0.4.3",
|
||||
"@expo/config-plugins": "1.0.21-alpha.0",
|
||||
"@expo/eas-build-job": "0.2.21",
|
||||
"@expo/eas-json": "^0.7.0",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "^1.7.0",
|
||||
|
||||
@@ -48,7 +48,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -61,7 +61,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -75,7 +75,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -93,7 +93,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -111,7 +111,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -126,7 +126,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
@@ -147,7 +147,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -165,7 +165,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, false);
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, false);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
@@ -186,7 +186,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
|
||||
await ensureCredentialsAsync(provider, Workflow.GENERIC, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Credentials for this app are not configured');
|
||||
@@ -204,7 +204,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -217,7 +217,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -231,7 +231,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -247,7 +247,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
Workflow.MANAGED,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
@@ -265,7 +265,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.LOCAL,
|
||||
false
|
||||
);
|
||||
@@ -283,7 +283,7 @@ describe(ensureCredentialsAsync, () => {
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
Workflow.GENERIC,
|
||||
CredentialsSource.REMOTE,
|
||||
false
|
||||
);
|
||||
|
||||
@@ -90,3 +90,21 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
|
||||
throw new Error('Missing values in AndroidManifest.xml');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.RELEASE_CHANNEL
|
||||
);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,12 @@ function isApplicationIdValid(applicationId: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
|
||||
}
|
||||
|
||||
export async function ensureApplicationIdIsValidAsync(projectDir: string) {
|
||||
const applicationId = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.Android);
|
||||
export async function ensureApplicationIdIsValidAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const applicationId = await ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir,
|
||||
platform: Platform.ANDROID,
|
||||
exp,
|
||||
});
|
||||
if (!isApplicationIdValid(applicationId)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
Log.error(
|
||||
|
||||
@@ -22,15 +22,15 @@ export async function prepareAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
const buildCtx = createBuildContext<Platform.Android>({
|
||||
const buildCtx = createBuildContext<Platform.ANDROID>({
|
||||
commandCtx,
|
||||
platform: Platform.Android,
|
||||
platform: Platform.ANDROID,
|
||||
easConfig,
|
||||
});
|
||||
const { buildProfile } = buildCtx;
|
||||
|
||||
if (
|
||||
buildProfile.workflow === Workflow.Generic &&
|
||||
buildProfile.workflow === Workflow.GENERIC &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'android')))
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -41,7 +41,7 @@ export async function prepareAndroidBuildAsync(
|
||||
}
|
||||
|
||||
if (
|
||||
buildProfile.workflow === Workflow.Generic &&
|
||||
buildProfile.workflow === Workflow.GENERIC &&
|
||||
buildProfile.distribution === 'internal' &&
|
||||
buildProfile.gradleCommand?.match(/bundle/)
|
||||
) {
|
||||
@@ -60,14 +60,14 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
}
|
||||
}
|
||||
|
||||
await ensureApplicationIdIsValidAsync(commandCtx.projectDir);
|
||||
await ensureApplicationIdIsValidAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {},
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
}
|
||||
},
|
||||
@@ -75,15 +75,15 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
});
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.Android>): boolean {
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
|
||||
return (
|
||||
ctx.buildProfile.workflow === Workflow.Managed ||
|
||||
(ctx.buildProfile.workflow === Workflow.Generic && !ctx.buildProfile.withoutCredentials)
|
||||
ctx.buildProfile.workflow === Workflow.MANAGED ||
|
||||
(ctx.buildProfile.workflow === Workflow.GENERIC && !ctx.buildProfile.withoutCredentials)
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
ctx: BuildContext<Platform.Android>
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
|
||||
}
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
await configureApplicationIdAsync(ctx.projectDir, ctx.exp, ctx.allowExperimental);
|
||||
await ensureApplicationIdIsValidAsync(ctx.projectDir);
|
||||
await ensureApplicationIdIsValidAsync(ctx.projectDir, ctx.exp);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await gitAddAsync(easGradlePath, { intentToAdd: true });
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Android,
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
@@ -22,13 +23,13 @@ interface JobData {
|
||||
}
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
if (ctx.buildProfile.workflow === Workflow.Generic) {
|
||||
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else if (ctx.buildProfile.workflow === Workflow.Managed) {
|
||||
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else {
|
||||
@@ -37,9 +38,10 @@ export async function prepareJobAsync(
|
||||
}
|
||||
|
||||
interface CommonJobProperties {
|
||||
platform: Platform.Android;
|
||||
platform: Platform.ANDROID;
|
||||
projectArchive: ArchiveSource;
|
||||
builderEnvironment: Android.BuilderEnvironment;
|
||||
cache: Cache | null;
|
||||
secrets: {
|
||||
buildCredentials?: {
|
||||
keystore: Android.Keystore;
|
||||
@@ -49,7 +51,7 @@ interface CommonJobProperties {
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
|
||||
@@ -68,7 +70,7 @@ async function prepareJobCommonAsync(
|
||||
: {};
|
||||
|
||||
return {
|
||||
platform: Platform.Android,
|
||||
platform: Platform.ANDROID,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: jobData.archiveBucketKey,
|
||||
@@ -80,6 +82,7 @@ async function prepareJobCommonAsync(
|
||||
ndk: ctx.buildProfile.ndk,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
secrets: {
|
||||
...(secretEnvs ? { secretEnvs } : {}),
|
||||
...buildCredentials,
|
||||
@@ -88,7 +91,7 @@ async function prepareJobCommonAsync(
|
||||
}
|
||||
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidGenericBuildProfile
|
||||
): Promise<Partial<Android.GenericJob>> {
|
||||
@@ -96,7 +99,7 @@ async function prepareGenericJobAsync(
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.Generic,
|
||||
type: Workflow.GENERIC,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
@@ -105,16 +108,16 @@ async function prepareGenericJobAsync(
|
||||
}
|
||||
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidManagedBuildProfile
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.Managed,
|
||||
type: Workflow.MANAGED,
|
||||
username: accountName,
|
||||
buildType: buildProfile.buildType,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { apiClient } from '../api';
|
||||
import Log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { UploadType, uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { platformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
@@ -14,6 +15,7 @@ import { AnalyticsEvent, Platform, TrackingContext } from './types';
|
||||
import Analytics from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import {
|
||||
commitChangedFilesAsync,
|
||||
commitPromptAsync,
|
||||
isGitStatusCleanAsync,
|
||||
makeProjectTarballAsync,
|
||||
@@ -112,8 +114,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
const body = error?.response?.body;
|
||||
if (body && JSON.parse(body)?.errors?.[0]?.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
if (error?.expoApiV2ErrorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
|
||||
}
|
||||
throw error;
|
||||
@@ -136,7 +137,10 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
message: 'Uploading to EAS Build',
|
||||
message: ratio =>
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: `Uploaded to EAS`,
|
||||
})
|
||||
);
|
||||
@@ -183,12 +187,17 @@ enum ShouldCommitChanges {
|
||||
}
|
||||
|
||||
async function reviewAndCommitChangesAsync(
|
||||
commitMessage: string,
|
||||
initialCommitMessage: string,
|
||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||
): Promise<void> {
|
||||
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
||||
await commitChangedFilesAsync(initialCommitMessage);
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
}
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
'Cannot commit changes when --non-interactive is specified. Run the command in interactive mode to review and commit changes.'
|
||||
'Cannot commit changes when --non-interactive is specified. Run the command in interactive mode or set EAS_BUILD_AUTOCOMMIT=1 in your environment.'
|
||||
);
|
||||
}
|
||||
const { selected } = await promptAsync({
|
||||
@@ -212,10 +221,13 @@ async function reviewAndCommitChangesAsync(
|
||||
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
|
||||
);
|
||||
} else if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync(commitMessage);
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(commitMessage, { nonInteractive, askedFirstTime: false });
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, {
|
||||
nonInteractive,
|
||||
askedFirstTime: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ enum ShouldCommitChanges {
|
||||
}
|
||||
|
||||
async function reviewAndCommitChangesAsync(
|
||||
commitMessage: string,
|
||||
initialCommitMessage: string,
|
||||
askedFirstTime: boolean = true
|
||||
): Promise<void> {
|
||||
const { selected } = await promptAsync({
|
||||
@@ -152,10 +152,10 @@ async function reviewAndCommitChangesAsync(
|
||||
});
|
||||
|
||||
if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync(commitMessage);
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(commitMessage, false);
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, false);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,6 @@ export const platformDisplayNames = {
|
||||
};
|
||||
|
||||
export const platformEmojis = {
|
||||
[Platform.iOS]: '🍎',
|
||||
[Platform.Android]: '🤖',
|
||||
[Platform.IOS]: '🍎',
|
||||
[Platform.ANDROID]: '🤖',
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidBuildProfile, EasConfig, iOSBuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
@@ -25,6 +25,7 @@ export interface CommandContext {
|
||||
export async function createCommandContextAsync({
|
||||
requestedPlatform,
|
||||
profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
trackingCtx,
|
||||
@@ -35,6 +36,7 @@ export async function createCommandContextAsync({
|
||||
}: {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile: string;
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
trackingCtx: TrackingContext;
|
||||
@@ -44,7 +46,6 @@ export async function createCommandContextAsync({
|
||||
waitForBuildEnd: boolean;
|
||||
}): Promise<CommandContext> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
|
||||
@@ -77,7 +78,7 @@ export interface ConfigureContext {
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
commandCtx,
|
||||
scheduledBuilds.map(i => i.buildId)
|
||||
);
|
||||
Log.newLine();
|
||||
printBuildResults(commandCtx.accountName, builds);
|
||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ async function startBuildsAsync(
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.Android, sendBuildRequestAsync });
|
||||
builds.push({ platform: Platform.ANDROID, sendBuildRequestAsync });
|
||||
}
|
||||
if (shouldBuildiOS) {
|
||||
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.iOS, sendBuildRequestAsync });
|
||||
builds.push({ platform: Platform.IOS, sendBuildRequestAsync });
|
||||
}
|
||||
return Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
@@ -88,7 +88,8 @@ async function waitForBuildEndAsync(
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
switch (builds[0]?.status) {
|
||||
const build = builds[0] as Build;
|
||||
switch (build.status) {
|
||||
case BuildStatus.FINISHED:
|
||||
spinner.succeed('Build finished');
|
||||
return builds;
|
||||
@@ -104,7 +105,11 @@ async function waitForBuildEndAsync(
|
||||
break;
|
||||
case BuildStatus.ERRORED:
|
||||
spinner.fail('Build failed');
|
||||
throw new Error(`Standalone build failed!`);
|
||||
if (build.error) {
|
||||
return builds;
|
||||
} else {
|
||||
throw new Error(`Standalone build failed!`);
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status.');
|
||||
throw new Error(`Unknown status: ${builds} - aborting!`);
|
||||
@@ -151,3 +156,10 @@ async function waitForBuildEndAsync(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (Build | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as Build[]).filter(i => i.status === 'errored');
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,13 @@ async function ensureCredentialsAutoAsync(
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
const platform = platformDisplayNames[provider.platform];
|
||||
switch (workflow) {
|
||||
case Workflow.Managed:
|
||||
case Workflow.MANAGED:
|
||||
if (await provider.hasLocalAsync()) {
|
||||
return CredentialsSource.LOCAL;
|
||||
} else {
|
||||
return CredentialsSource.REMOTE;
|
||||
}
|
||||
case Workflow.Generic: {
|
||||
case Workflow.GENERIC: {
|
||||
const hasLocal = await provider.hasLocalAsync();
|
||||
const hasRemote = await provider.hasRemoteAsync();
|
||||
if (hasRemote && hasLocal) {
|
||||
|
||||
@@ -86,3 +86,12 @@ async function writeExpoPlistAsync(
|
||||
await writePlistAsync(expoPlistPath, expoPlist);
|
||||
await gitAddAsync(expoPlistPath, { intentToAdd: true });
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -14,20 +14,20 @@ import { Platform } from '../types';
|
||||
import { ensureBundleIdentifierIsValidAsync } from './bundleIdentifer';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { prepareJobAsync, sanitizedTargetName } from './prepareJob';
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
const buildCtx = createBuildContext<Platform.iOS>({
|
||||
const buildCtx = createBuildContext<Platform.IOS>({
|
||||
commandCtx,
|
||||
platform: Platform.iOS,
|
||||
platform: Platform.IOS,
|
||||
easConfig,
|
||||
});
|
||||
|
||||
if (
|
||||
buildCtx.buildProfile.workflow === Workflow.Generic &&
|
||||
buildCtx.buildProfile.workflow === Workflow.GENERIC &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
|
||||
) {
|
||||
throw new Error(
|
||||
@@ -39,15 +39,23 @@ export async function prepareIosBuildAsync(
|
||||
|
||||
let iosBuildScheme: string | undefined;
|
||||
let iosApplicationTarget: string | undefined;
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
iosBuildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
iosApplicationTarget = await IOSConfig.BuildScheme.getApplicationTargetForSchemeAsync(
|
||||
buildCtx.commandCtx.projectDir,
|
||||
iosBuildScheme
|
||||
);
|
||||
} else {
|
||||
if (!commandCtx.exp.name) {
|
||||
throw new Error('"expo.name" is required in your app.json');
|
||||
}
|
||||
iosApplicationTarget = sanitizedTargetName(commandCtx.exp.name);
|
||||
if (!iosApplicationTarget) {
|
||||
throw new Error('"expo.name" needs to contain some alphanumeric characters');
|
||||
}
|
||||
}
|
||||
|
||||
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir);
|
||||
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
@@ -67,7 +75,7 @@ export async function prepareIosBuildAsync(
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<string> {
|
||||
async function resolveSchemeAsync(ctx: BuildContext<Platform.IOS>): Promise<string> {
|
||||
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
|
||||
if (schemes.length === 1) {
|
||||
return schemes[0];
|
||||
|
||||
@@ -21,8 +21,12 @@ function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(bundleIdentifier);
|
||||
}
|
||||
|
||||
export async function ensureBundleIdentifierIsValidAsync(projectDir: string) {
|
||||
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.iOS);
|
||||
export async function ensureBundleIdentifierIsValidAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir,
|
||||
platform: Platform.IOS,
|
||||
exp,
|
||||
});
|
||||
if (!isBundleIdentifierValid(bundleIdentifier)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
Log.error(
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { VersionAutoIncrement, iOSBuildProfile } from '@expo/eas-json';
|
||||
import {
|
||||
EasJsonReader,
|
||||
VersionAutoIncrement,
|
||||
iOSBuildProfile,
|
||||
iOSGenericBuildProfile,
|
||||
} from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import { ConfigureContext } from '../context';
|
||||
@@ -17,8 +22,14 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
return;
|
||||
}
|
||||
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
await ensureBundleIdentifierIsValidAsync(ctx.projectDir);
|
||||
|
||||
const disableIosBundleIdentifierValidation = (
|
||||
await new EasJsonReader(ctx.projectDir, 'all').readRawAsync()
|
||||
)?.experimental?.disableIosBundleIdentifierValidation;
|
||||
if (!disableIosBundleIdentifierValidation) {
|
||||
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
await ensureBundleIdentifierIsValidAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
@@ -38,14 +49,20 @@ export async function validateAndSyncProjectConfigurationAsync({
|
||||
const { workflow, autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
|
||||
|
||||
if (workflow === Workflow.Generic) {
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
if (!bundleIdentifierFromPbxproj || bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier) {
|
||||
throw new Error(
|
||||
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const genericBuildProfile = buildProfile as iOSGenericBuildProfile;
|
||||
if (!genericBuildProfile.disableIosBundleIdentifierValidation) {
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
if (
|
||||
!bundleIdentifierFromPbxproj ||
|
||||
bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier
|
||||
) {
|
||||
throw new Error(
|
||||
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ensureCredentialsAsync } from '../credentials';
|
||||
import { Platform } from '../types';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
ctx: BuildContext<Platform.iOS>
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<CredentialsResult<IosCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
@@ -68,6 +68,6 @@ export async function resolveIosCredentialsAsync(
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.iOS>): boolean {
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.IOS>): boolean {
|
||||
return ctx.buildProfile.distribution !== 'simulator';
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Cache,
|
||||
Ios,
|
||||
Job,
|
||||
Workflow,
|
||||
iOS,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
|
||||
@@ -31,13 +32,13 @@ interface JobData {
|
||||
}
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
if (ctx.buildProfile.workflow === Workflow.Generic) {
|
||||
if (ctx.buildProfile.workflow === Workflow.GENERIC) {
|
||||
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else if (ctx.buildProfile.workflow === Workflow.Managed) {
|
||||
} else if (ctx.buildProfile.workflow === Workflow.MANAGED) {
|
||||
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else {
|
||||
@@ -46,19 +47,20 @@ export async function prepareJobAsync(
|
||||
}
|
||||
|
||||
interface CommonJobProperties {
|
||||
platform: Platform.iOS;
|
||||
platform: Platform.IOS;
|
||||
projectArchive: ArchiveSource;
|
||||
builderEnvironment: iOS.BuilderEnvironment;
|
||||
releaseChannel: string;
|
||||
distribution?: iOS.DistributionType;
|
||||
builderEnvironment: Ios.BuilderEnvironment;
|
||||
releaseChannel?: string;
|
||||
distribution?: Ios.DistributionType;
|
||||
cache: Cache | null;
|
||||
secrets: {
|
||||
buildCredentials: iOS.BuildCredentials;
|
||||
buildCredentials: Ios.BuildCredentials;
|
||||
secretEnvs?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
{
|
||||
archiveBucketKey,
|
||||
credentials,
|
||||
@@ -84,7 +86,7 @@ async function prepareJobCommonAsync(
|
||||
}
|
||||
|
||||
return {
|
||||
platform: Platform.iOS,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: archiveBucketKey,
|
||||
@@ -94,10 +96,12 @@ async function prepareJobCommonAsync(
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
cocoapods: ctx.buildProfile.cocoapods,
|
||||
fastlane: ctx.buildProfile.fastlane,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: ctx.buildProfile.cache,
|
||||
secrets: {
|
||||
...(secretEnvs ? { secretEnvs } : {}),
|
||||
buildCredentials,
|
||||
@@ -105,7 +109,7 @@ async function prepareJobCommonAsync(
|
||||
};
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): iOS.TargetCredentials {
|
||||
function prepareTargetCredentials(targetCredentials: IosTargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
@@ -116,10 +120,10 @@ function prepareTargetCredentials(targetCredentials: IosTargetCredentials): iOS.
|
||||
}
|
||||
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSGenericBuildProfile
|
||||
): Promise<Partial<iOS.GenericJob>> {
|
||||
): Promise<Partial<Ios.GenericJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
return {
|
||||
@@ -128,9 +132,12 @@ async function prepareGenericJobAsync(
|
||||
credentials: jobData.credentials,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
})),
|
||||
type: Workflow.Generic,
|
||||
type: Workflow.GENERIC,
|
||||
scheme: jobData.projectConfiguration.iosBuildScheme,
|
||||
schemeBuildConfiguration: buildProfile.schemeBuildConfiguration,
|
||||
schemeBuildConfiguration:
|
||||
buildProfile.schemeBuildConfiguration === 'Auto'
|
||||
? undefined
|
||||
: buildProfile.schemeBuildConfiguration,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
@@ -138,21 +145,20 @@ async function prepareGenericJobAsync(
|
||||
}
|
||||
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSManagedBuildProfile
|
||||
): Promise<Partial<iOS.ManagedJob>> {
|
||||
): Promise<Partial<Ios.ManagedJob>> {
|
||||
const projectRootDirectory =
|
||||
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
|
||||
const targetName = sanitizedName(ctx.commandCtx.exp.name);
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.exp);
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, {
|
||||
archiveBucketKey: jobData.archiveBucketKey,
|
||||
credentials: jobData.credentials,
|
||||
targetName,
|
||||
targetName: jobData.projectConfiguration.iosApplicationTarget,
|
||||
})),
|
||||
type: Workflow.Managed,
|
||||
type: Workflow.MANAGED,
|
||||
buildType: buildProfile.buildType,
|
||||
username: accountName,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
@@ -162,7 +168,7 @@ async function prepareManagedJobAsync(
|
||||
|
||||
// copy-pasted from expo-cli/packages/xdl/src/Exp.ts
|
||||
// it's used in eject
|
||||
function sanitizedName(name: string) {
|
||||
export function sanitizedTargetName(name: string) {
|
||||
return name
|
||||
.replace(/[\W_]+/g, '')
|
||||
.normalize('NFD')
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Metadata } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
|
||||
import { getAppIdentifierAsync } from '../project/projectUtils';
|
||||
import { gitCommitHashAsync } from '../utils/git';
|
||||
import { readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync } from './android/UpdatesModule';
|
||||
import { BuildContext } from './context';
|
||||
import { BuildMetadata, Platform } from './types';
|
||||
import { readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync } from './ios/UpdatesModule';
|
||||
import { Platform } from './types';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
/**
|
||||
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
|
||||
@@ -18,17 +23,44 @@ export async function collectMetadata<T extends Platform>(
|
||||
}: {
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
}
|
||||
): Promise<BuildMetadata> {
|
||||
): Promise<Metadata> {
|
||||
return {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
appVersion: ctx.commandCtx.exp.version!,
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource,
|
||||
sdkVersion: ctx.commandCtx.exp.sdkVersion,
|
||||
trackingContext: ctx.trackingCtx,
|
||||
releaseChannel: await resolveReleaseChannel(ctx),
|
||||
distribution: ctx.buildProfile.distribution ?? 'store',
|
||||
appName: ctx.commandCtx.exp.name,
|
||||
appIdentifier:
|
||||
(await getAppIdentifierAsync(ctx.commandCtx.projectDir, ctx.platform)) ?? undefined,
|
||||
(await getAppIdentifierAsync({
|
||||
projectDir: ctx.commandCtx.projectDir,
|
||||
platform: ctx.platform,
|
||||
exp: ctx.commandCtx.exp,
|
||||
})) ?? undefined,
|
||||
buildProfile: ctx.commandCtx.profile,
|
||||
gitCommitHash: await gitCommitHashAsync(),
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveReleaseChannel<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
if (!isExpoUpdatesInstalled(ctx.commandCtx.projectDir)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return ctx.buildProfile.releaseChannel;
|
||||
}
|
||||
|
||||
let maybeReleaseChannel: string | null;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
maybeReleaseChannel = await readAndroidReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
} else {
|
||||
maybeReleaseChannel = await readIosReleaseChannelSafelyAsync(ctx.commandCtx.projectDir);
|
||||
}
|
||||
return maybeReleaseChannel ?? 'default';
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
AndroidBuildProfile,
|
||||
CredentialsSource,
|
||||
DistributionType,
|
||||
iOSBuildProfile,
|
||||
} from '@expo/eas-json';
|
||||
import { Metadata, Platform, errors } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, iOSBuildProfile } from '@expo/eas-json';
|
||||
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
@@ -31,7 +26,8 @@ export interface Build {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
artifacts?: BuildArtifacts;
|
||||
metadata?: Partial<BuildMetadata>;
|
||||
metadata?: Partial<Metadata>;
|
||||
error?: errors.ExternalUserError;
|
||||
}
|
||||
|
||||
interface BuildArtifacts {
|
||||
@@ -39,68 +35,7 @@ interface BuildArtifacts {
|
||||
logsUrl: string;
|
||||
}
|
||||
|
||||
export type BuildMetadata = {
|
||||
/**
|
||||
* Application version (the expo.version key in app.json/app.config.js)
|
||||
*/
|
||||
appVersion: string;
|
||||
|
||||
/**
|
||||
* EAS CLI version
|
||||
*/
|
||||
cliVersion: string;
|
||||
|
||||
/**
|
||||
* Build workflow
|
||||
* It's either 'generic' or 'managed'
|
||||
*/
|
||||
workflow: Workflow;
|
||||
|
||||
/**
|
||||
* Credentials source
|
||||
* Credentials could be obtained either from credential.json or EAS servers.
|
||||
*/
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
|
||||
/**
|
||||
* Expo SDK version
|
||||
* It's determined by the expo package version in package.json.
|
||||
* It's undefined if the expo package is not installed for the project.
|
||||
*/
|
||||
sdkVersion?: string;
|
||||
|
||||
/**
|
||||
* Release channel (for expo-updates)
|
||||
* It's undefined if the expo-updates package is not installed for the project.
|
||||
*/
|
||||
releaseChannel?: string;
|
||||
|
||||
/**
|
||||
* Tracking context
|
||||
* It's used to track build process across different Expo services and tools.
|
||||
*/
|
||||
trackingContext: TrackingContext;
|
||||
|
||||
/**
|
||||
* Distribution type
|
||||
* Indicates whether this is a build for store, internal distribution, or simulator (iOS).
|
||||
*/
|
||||
distribution: DistributionType;
|
||||
|
||||
/**
|
||||
* App name (expo.name in app.json/app.config.js)
|
||||
*/
|
||||
appName?: string;
|
||||
|
||||
/**
|
||||
* App identifier:
|
||||
* - iOS builds: the bundle identifier (expo.ios.bundleIdentifier in app.json/app.config.js)
|
||||
* - Android builds: the application id (expo.android.package in app.json/app.config.js)
|
||||
*/
|
||||
appIdentifier?: string;
|
||||
};
|
||||
|
||||
export type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
|
||||
export type PlatformBuildProfile<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { errors } from '@expo/eas-build-job';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { platformDisplayNames, platformEmojis } from '../constants';
|
||||
import { Build } from '../types';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
@@ -34,18 +35,37 @@ export function printLogsUrls(
|
||||
}
|
||||
|
||||
export function printBuildResults(accountName: string, builds: (Build | null)[]): void {
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(accountName, build);
|
||||
} else {
|
||||
(builds.filter(i => i) as Build[])
|
||||
.filter(build => build.status === 'finished')
|
||||
.forEach(build => printBuildResult(accountName, build));
|
||||
(builds.filter(i => i) as Build[]).forEach(build => printBuildResult(accountName, build));
|
||||
}
|
||||
}
|
||||
|
||||
function printBuildResult(accountName: string, build: Build): void {
|
||||
Log.addNewLineIfNone();
|
||||
if (build.status === 'errored') {
|
||||
const userError = build.error;
|
||||
Log.error(
|
||||
`${platformEmojis[build.platform]} ${platformDisplayNames[build.platform]} build failed${
|
||||
userError ? ':' : ''
|
||||
}`
|
||||
);
|
||||
if (userError) {
|
||||
printUserError(userError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (build.status === 'canceled') {
|
||||
Log.error(
|
||||
`${platformEmojis[build.platform]} ${platformDisplayNames[build.platform]} build was canceled`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (build.metadata?.distribution === 'internal') {
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId: build.id,
|
||||
@@ -57,14 +77,12 @@ function printBuildResult(accountName: string, build: Build): void {
|
||||
} devices to install the app:`
|
||||
);
|
||||
Log.log(`${chalk.underline(logsUrl)}`);
|
||||
Log.newLine();
|
||||
} else {
|
||||
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
|
||||
// we should account for this case better if it is possible
|
||||
const url = build.artifacts?.buildUrl ?? '';
|
||||
Log.log(`${platformEmojis[build.platform]} ${platformDisplayNames[build.platform]} app:`);
|
||||
Log.log(`${chalk.underline(url)}`);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,3 +105,10 @@ export function printDeprecationWarnings(deprecationInfo?: DeprecationInfo): voi
|
||||
Log.warn(deprecationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export function printUserError(error: errors.ExternalUserError): void {
|
||||
Log.error(error.message);
|
||||
if (error.docsUrl) {
|
||||
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Log from '../../log';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import {
|
||||
doesGitRepoExistAsync,
|
||||
getGitDiffOutputAsync,
|
||||
@@ -43,12 +44,11 @@ async function ensureGitRepoExistsAsync(): Promise<void> {
|
||||
|
||||
Log.log("We're going to make an initial commit for you repository.");
|
||||
|
||||
await spawnAsync('git', ['add', '-A']);
|
||||
await commitPromptAsync('Initial commit');
|
||||
await commitPromptAsync({ initialCommitMessage: 'Initial commit', commitAllFiles: true });
|
||||
}
|
||||
|
||||
async function isGitStatusCleanAsync(): Promise<boolean> {
|
||||
const changes = await gitStatusAsync();
|
||||
const changes = await gitStatusAsync({ showUntracked: true });
|
||||
return changes.length === 0;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
|
||||
message: `Commit changes to git?`,
|
||||
});
|
||||
if (answer) {
|
||||
await commitPromptAsync();
|
||||
await commitPromptAsync({ commitAllFiles: true });
|
||||
} else {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
@@ -120,7 +120,6 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
try {
|
||||
await spawnAsync('git', [
|
||||
'clone',
|
||||
'--local',
|
||||
'--no-hardlinks',
|
||||
'--depth',
|
||||
'1',
|
||||
@@ -130,19 +129,26 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
} finally {
|
||||
// Stop the timer
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
await fs.remove(shallowClonePath);
|
||||
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(`Compressed project files ${chalk.dim(prettyTime)}`);
|
||||
spinner.fail();
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
await fs.remove(shallowClonePath);
|
||||
}
|
||||
clearTimeout(timer);
|
||||
|
||||
const { size } = await fs.stat(tarPath);
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(
|
||||
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
|
||||
);
|
||||
}
|
||||
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
@@ -151,7 +157,13 @@ async function showDiffAsync() {
|
||||
await gitDiffAsync({ withPager: outputTooLarge });
|
||||
}
|
||||
|
||||
async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
|
||||
async function commitPromptAsync({
|
||||
initialCommitMessage,
|
||||
commitAllFiles,
|
||||
}: {
|
||||
initialCommitMessage?: string;
|
||||
commitAllFiles?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
@@ -159,6 +171,9 @@ async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
if (commitAllFiles) {
|
||||
await spawnAsync('git', ['add', '-A']);
|
||||
}
|
||||
await commitChangedFilesAsync(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { UpdateBranch } from '../../graphql/generated';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
@@ -15,16 +19,10 @@ import { getBranchNameAsync } from '../../utils/git';
|
||||
export async function createUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
name,
|
||||
}: {
|
||||
appId: string;
|
||||
name: string;
|
||||
}): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
}: CreateUpdateBranchForAppMutationVariables): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ updateBranch: { createUpdateBranchForApp: Pick<UpdateBranch, 'id' | 'name'> } },
|
||||
{ appId: string; name: string }
|
||||
>(
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
gql`
|
||||
mutation createUpdateBranchForApp($appId: ID!, $name: String!) {
|
||||
updateBranch {
|
||||
@@ -42,7 +40,11 @@ export async function createUpdateBranchOnAppAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateBranch.createUpdateBranchForApp;
|
||||
const newBranch = result.updateBranch.createUpdateBranchForApp;
|
||||
if (!newBranch) {
|
||||
throw new Error(`Could not create branch ${name}.`);
|
||||
}
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
export default class BranchCreate extends Command {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateBranchMutation,
|
||||
DeleteUpdateBranchMutationVariables,
|
||||
DeleteUpdateBranchResult,
|
||||
GetBranchInfoQuery,
|
||||
GetBranchInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
async function getBranchInfoAsync({
|
||||
appId,
|
||||
name,
|
||||
}: GetBranchInfoQueryVariables): Promise<{ branchId: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetBranchInfoQuery, GetBranchInfoQueryVariables>(
|
||||
gql`
|
||||
query GetBranchInfo($appId: String!, $name: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateBranchByName(name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const branchId = data.app?.byId.updateBranchByName.id;
|
||||
if (!branchId) {
|
||||
throw new Error(`Could not find branch ${name} on ${appId}`);
|
||||
}
|
||||
return { branchId };
|
||||
}
|
||||
|
||||
async function deleteBranchOnAppAsync({
|
||||
branchId,
|
||||
}: DeleteUpdateBranchMutationVariables): Promise<DeleteUpdateBranchResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateBranchMutation, DeleteUpdateBranchMutationVariables>(
|
||||
gql`
|
||||
mutation DeleteUpdateBranch($branchId: ID!) {
|
||||
updateBranch {
|
||||
deleteUpdateBranch(branchId: $branchId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
branchId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateBranch.deleteUpdateBranch;
|
||||
}
|
||||
|
||||
export default class BranchDelete extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Republish an update group';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: false,
|
||||
description: 'Name of the branch to delete',
|
||||
},
|
||||
];
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: `return JSON with the edited branch's ID and name.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
let {
|
||||
args: { name },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(BranchDelete);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = await getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: exp.slug,
|
||||
});
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the branch to delete:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const { branchId } = await getBranchInfoAsync({ appId: projectId, name });
|
||||
|
||||
if (!jsonFlag) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permamently delete branch: "${name}" and all of the updates published on it.` +
|
||||
`\nThis action is irreversable.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error(`Cancelled deletion of branch: "${name}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const deletionResult = await deleteBranchOnAppAsync({
|
||||
branchId,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(deletionResult));
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(
|
||||
`@${accountName}/${exp.slug}`
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,27 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { CLIError } from '@oclif/errors';
|
||||
import CliTable from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
import { format } from 'timeago.js';
|
||||
|
||||
import { graphqlClient } from '../../graphql/client';
|
||||
import { RootQuery, Update, UpdateBranch } from '../../graphql/generated';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
BranchesByAppQuery,
|
||||
BranchesByAppQueryVariables,
|
||||
Maybe,
|
||||
Robot,
|
||||
Update,
|
||||
User,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
|
||||
import { getActorDisplayName } from '../../user/actions';
|
||||
|
||||
const BRANCHES_LIMIT = 10_000;
|
||||
|
||||
export default class BranchList extends Command {
|
||||
static hidden = true;
|
||||
|
||||
static description = 'List all branches on this project.';
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: 'return output as JSON',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { flags } = this.parse(BranchList);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const fullName = await getProjectFullNameAsync(projectDir);
|
||||
const branches = await this.listBranchesAsync({ fullName });
|
||||
if (flags.json) {
|
||||
Log.log(JSON.stringify(branches, null, 2));
|
||||
} else {
|
||||
const table = new CliTable({ head: ['Branch', 'Latest update'] });
|
||||
table.push(
|
||||
...branches.map((branch: UpdateBranch) => [branch.name, formatUpdate(branch.updates[0])])
|
||||
);
|
||||
Log.log(table.toString());
|
||||
if (branches.length >= BRANCHES_LIMIT) {
|
||||
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async listBranchesAsync({ fullName }: { fullName: string }): Promise<UpdateBranch[]> {
|
||||
const { data, error } = await graphqlClient
|
||||
.query<RootQuery>(
|
||||
export async function listBranchesAsync({ fullName }: { fullName: string }) {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<BranchesByAppQuery, BranchesByAppQueryVariables>(
|
||||
gql`
|
||||
query BranchesByAppQuery($fullName: String!, $limit: Int!) {
|
||||
app {
|
||||
@@ -84,21 +56,52 @@ export default class BranchList extends Command {
|
||||
limit: BRANCHES_LIMIT,
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error.networkError) {
|
||||
throw new CLIError(`Fetching branches failed: ${error.networkError.message}`);
|
||||
} else {
|
||||
throw new CLIError(error.graphQLErrors.map(e => e.message).join('\n'));
|
||||
return data?.app?.byFullName.updateBranches ?? [];
|
||||
}
|
||||
|
||||
export default class BranchList extends Command {
|
||||
static hidden = true;
|
||||
|
||||
static description = 'List all branches on this project.';
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: 'return output as JSON',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const { flags } = this.parse(BranchList);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const fullName = await getProjectFullNameAsync(exp);
|
||||
const branches = await listBranchesAsync({ fullName });
|
||||
if (flags.json) {
|
||||
Log.log(JSON.stringify(branches, null, 2));
|
||||
} else {
|
||||
const table = new CliTable({ head: ['Branch', 'Latest update'] });
|
||||
table.push(...branches.map(branch => [branch.name, formatUpdate(branch.updates[0])]));
|
||||
Log.log(table.toString());
|
||||
if (branches.length >= BRANCHES_LIMIT) {
|
||||
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
|
||||
}
|
||||
}
|
||||
|
||||
return data?.app?.byFullName.updateBranches ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatUpdate(update: Update | undefined): string {
|
||||
export function formatUpdate(
|
||||
update: Pick<Update, 'id' | 'updatedAt' | 'message'> & {
|
||||
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
}
|
||||
): string {
|
||||
if (!update) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
@@ -1,58 +1,123 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import dateFormat from 'dateformat';
|
||||
import gql from 'graphql-tag';
|
||||
import { uniqBy } from 'lodash';
|
||||
import ora from 'ora';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
Actor,
|
||||
GetUpdateGroupAsyncQuery,
|
||||
RootQueryUpdatesByGroupArgs,
|
||||
Update,
|
||||
UpdateInfoGroup,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { buildUpdateInfoGroupAsync, collectAssets, uploadAssetsAsync } from '../../project/publish';
|
||||
import { promptAsync } from '../../prompts';
|
||||
buildBundlesAsync,
|
||||
buildUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { promptAsync, selectAsync } from '../../prompts';
|
||||
import { getLastCommitMessageAsync } from '../../utils/git';
|
||||
import { formatUpdate, listBranchesAsync } from './list';
|
||||
import { viewUpdateBranchAsync } from './view';
|
||||
|
||||
type PublishPlatforms = 'android' | 'ios';
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return updatesByGroup;
|
||||
}
|
||||
|
||||
export default class BranchPublish extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Publish an update group to a branch.';
|
||||
|
||||
static flags = {
|
||||
'input-dir': flags.string({
|
||||
description: 'location of the bundle',
|
||||
default: 'dist',
|
||||
required: false,
|
||||
}),
|
||||
branch: flags.string({
|
||||
description: 'current name of the branch.',
|
||||
description: 'name of the branch to publish on.',
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: `return a json with the edited branch's ID and name.`,
|
||||
description: `return a json with the new update group.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
let {
|
||||
flags: { json: jsonFlag, branch: name, message, 'input-dir': inputDir },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
branch: name,
|
||||
message,
|
||||
republish,
|
||||
group,
|
||||
'input-dir': inputDir,
|
||||
'skip-bundler': skipBundler,
|
||||
},
|
||||
} = this.parse(BranchPublish);
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const {
|
||||
exp: { slug, runtimeVersion },
|
||||
} = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug, runtimeVersion } = exp;
|
||||
if (!runtimeVersion) {
|
||||
throw new Error(
|
||||
"Couldn't find 'runtimeVersion'. Please specify it under the 'expo' key in 'app.json'"
|
||||
@@ -68,12 +133,79 @@ export default class BranchPublish extends Command {
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the branch to publish on:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
|
||||
const branches = await listBranchesAsync({ fullName: `@${accountName}/${slug}` });
|
||||
name = 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(name, 'branch name must be specified.');
|
||||
|
||||
const { id: branchId, updates } = await viewUpdateBranchAsync({
|
||||
appId: projectId,
|
||||
name,
|
||||
});
|
||||
|
||||
let updateInfoGroup: 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 {
|
||||
const updateGroups = uniqBy(updates, u => u.group).map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${name}". Did you mean to do a regular publish?`
|
||||
);
|
||||
}
|
||||
const updateGroup = await selectAsync<string>(
|
||||
'which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
updatesToRepublish = updates.filter(update => update.group === updateGroup);
|
||||
}
|
||||
|
||||
for (const update of updatesToRepublish) {
|
||||
const { platform, manifestFragment } = update;
|
||||
updateInfoGroup[platform as PublishPlatforms] = JSON.parse(manifestFragment);
|
||||
}
|
||||
// These are the same for each member of an update group
|
||||
group = updatesToRepublish[0].group;
|
||||
oldMessage = updatesToRepublish[0].message ?? '';
|
||||
oldRuntimeVersion = updatesToRepublish[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 assets = collectAssets(inputDir!);
|
||||
await uploadAssetsAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
@@ -85,35 +217,20 @@ export default class BranchPublish extends Command {
|
||||
type: 'text',
|
||||
name: 'publishMessage',
|
||||
message: `Please enter a publication message.`,
|
||||
initial: (await getLastCommitMessageAsync())?.trim(),
|
||||
initial: republish
|
||||
? `Republish "${oldMessage!}" - group: ${group}`
|
||||
: (await getLastCommitMessageAsync())?.trim(),
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const { id: branchId } = await getBranchByNameAsync({
|
||||
appId: projectId,
|
||||
name: name!,
|
||||
});
|
||||
|
||||
let updateInfoGroup;
|
||||
const assetSpinner = ora('Uploading assets...').start();
|
||||
try {
|
||||
const assets = collectAssets(inputDir!);
|
||||
await uploadAssetsAsync(assets);
|
||||
updateInfoGroup = await buildUpdateInfoGroupAsync(assets);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
throw e;
|
||||
}
|
||||
|
||||
let newUpdateGroup;
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdateGroup = await PublishMutation.publishUpdateGroupAsync({
|
||||
branchId,
|
||||
updateInfoGroup,
|
||||
runtimeVersion,
|
||||
runtimeVersion: republish ? oldRuntimeVersion! : runtimeVersion,
|
||||
message,
|
||||
});
|
||||
publishSpinner.succeed('Published!');
|
||||
@@ -123,7 +240,7 @@ export default class BranchPublish extends Command {
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(newUpdateGroup);
|
||||
Log.log(JSON.stringify(newUpdateGroup));
|
||||
} else {
|
||||
const outputMessage = new Table({
|
||||
wordWrap: true,
|
||||
@@ -157,3 +274,14 @@ export default class BranchPublish extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatUpdateTitle(
|
||||
update: Pick<Update, 'message' | 'createdAt' | 'runtimeVersion'> & {
|
||||
actor?: Pick<Actor, 'firstName'> | null;
|
||||
}
|
||||
): string {
|
||||
const { message, createdAt, actor, runtimeVersion } = update;
|
||||
return `[${dateFormat(createdAt, 'mmm dd HH:MM')} by ${
|
||||
actor?.firstName ?? 'unknown'
|
||||
}, runtimeVersion: ${runtimeVersion}] ${message}`;
|
||||
}
|
||||
@@ -4,7 +4,12 @@ import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { EditUpdateBranchInput, UpdateBranch } from '../../graphql/generated';
|
||||
import {
|
||||
EditUpdateBranchInput,
|
||||
EditUpdateBranchMutation,
|
||||
EditUpdateBranchMutationVariables,
|
||||
UpdateBranch,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
|
||||
@@ -13,21 +18,12 @@ import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
async function renameUpdateBranchOnAppAsync({
|
||||
appId,
|
||||
currentName,
|
||||
name,
|
||||
newName,
|
||||
}: {
|
||||
appId: string;
|
||||
currentName: string;
|
||||
newName: string;
|
||||
}): Promise<UpdateBranch> {
|
||||
}: EditUpdateBranchInput): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ updateBranch: { editUpdateBranch: UpdateBranch } },
|
||||
{
|
||||
input: EditUpdateBranchInput;
|
||||
}
|
||||
>(
|
||||
.mutation<EditUpdateBranchMutation, EditUpdateBranchMutationVariables>(
|
||||
gql`
|
||||
mutation EditUpdateBranch($input: EditUpdateBranchInput!) {
|
||||
updateBranch {
|
||||
@@ -41,7 +37,7 @@ async function renameUpdateBranchOnAppAsync({
|
||||
{
|
||||
input: {
|
||||
appId,
|
||||
name: currentName,
|
||||
name,
|
||||
newName,
|
||||
},
|
||||
}
|
||||
@@ -114,12 +110,12 @@ export default class BranchRename extends Command {
|
||||
|
||||
const editedBranch = await renameUpdateBranchOnAppAsync({
|
||||
appId: projectId,
|
||||
currentName: currentName!,
|
||||
name: currentName!,
|
||||
newName: newName!,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(editedBranch);
|
||||
Log.log(JSON.stringify(editedBranch));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,15 @@ import gql from 'graphql-tag';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { Update } from '../../graphql/generated';
|
||||
import {
|
||||
Maybe,
|
||||
Robot,
|
||||
Update,
|
||||
UpdateBranch,
|
||||
User,
|
||||
ViewBranchQuery,
|
||||
ViewBranchQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
@@ -16,37 +24,22 @@ const PAGE_LIMIT = 10_000;
|
||||
|
||||
type TruncatedUpdate = Pick<Update, 'group' | 'message' | 'createdAt' | 'actor'>;
|
||||
|
||||
async function viewUpdateBranchAsync({
|
||||
export async function viewUpdateBranchAsync({
|
||||
appId,
|
||||
name,
|
||||
}: {
|
||||
appId: string;
|
||||
name: string;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
updates: TruncatedUpdate[];
|
||||
}> {
|
||||
}: Pick<ViewBranchQueryVariables, 'appId' | 'name'>): Promise<
|
||||
Pick<UpdateBranch, 'id' | 'name'> & {
|
||||
updates: (Pick<
|
||||
Update,
|
||||
'id' | 'group' | 'message' | 'createdAt' | 'runtimeVersion' | 'platform' | 'manifestFragment'
|
||||
> & {
|
||||
actor?: Maybe<Pick<User, 'firstName' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
})[];
|
||||
}
|
||||
> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{
|
||||
app: {
|
||||
byId: {
|
||||
updateBranchByName: {
|
||||
id: string;
|
||||
name: string;
|
||||
updates: TruncatedUpdate[];
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
{
|
||||
appId: string;
|
||||
name: string;
|
||||
limit: number;
|
||||
}
|
||||
>(
|
||||
.query<ViewBranchQuery, ViewBranchQueryVariables>(
|
||||
gql`
|
||||
query ViewBranch($appId: String!, $name: String!, $limit: Int!) {
|
||||
app {
|
||||
@@ -60,6 +53,9 @@ async function viewUpdateBranchAsync({
|
||||
group
|
||||
message
|
||||
createdAt
|
||||
runtimeVersion
|
||||
platform
|
||||
manifestFragment
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
@@ -83,7 +79,11 @@ async function viewUpdateBranchAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.app.byId.updateBranchByName;
|
||||
const updateBranch = data.app?.byId.updateBranchByName;
|
||||
if (!updateBranch) {
|
||||
throw new Error(`Could not find branch "${name}"`);
|
||||
}
|
||||
return updateBranch;
|
||||
}
|
||||
|
||||
export default class BranchView extends Command {
|
||||
@@ -115,10 +115,10 @@ export default class BranchView extends Command {
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const {
|
||||
exp: { slug },
|
||||
} = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
@@ -147,7 +147,7 @@ export default class BranchView extends Command {
|
||||
);
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log({ ...UpdateBranch, updates });
|
||||
Log.log(JSON.stringify({ ...UpdateBranch, updates }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
@@ -6,7 +7,13 @@ import ora from 'ora';
|
||||
import { platformEmojis } from '../../build/constants';
|
||||
import { Platform } from '../../build/types';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { AppPlatform, Build, BuildStatus } from '../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
Build,
|
||||
BuildStatus,
|
||||
CancelBuildMutation,
|
||||
CancelBuildMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -19,7 +26,7 @@ import { confirmAsync, selectAsync } from '../../prompts';
|
||||
async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<{ build: { cancel: { id: string; status: BuildStatus } } }>(
|
||||
.mutation<CancelBuildMutation, CancelBuildMutationVariables>(
|
||||
gql`
|
||||
mutation CancelBuildMutation($buildId: ID!) {
|
||||
build(buildId: $buildId) {
|
||||
@@ -38,8 +45,8 @@ async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'st
|
||||
}
|
||||
|
||||
const appPlatformMap = {
|
||||
[AppPlatform.Android]: Platform.Android,
|
||||
[AppPlatform.Ios]: Platform.iOS,
|
||||
[AppPlatform.Android]: Platform.ANDROID,
|
||||
[AppPlatform.Ios]: Platform.IOS,
|
||||
};
|
||||
|
||||
function formatUnfinishedBuild(
|
||||
@@ -109,8 +116,9 @@ export default class BuildCancel extends Command {
|
||||
const { BUILD_ID: buildIdFromArg } = this.parse(BuildCancel).args;
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectId = await getProjectIdAsync(projectDir);
|
||||
const projectFullName = await getProjectFullNameAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
|
||||
if (buildIdFromArg) {
|
||||
await ensureBuildExistsAsync(buildIdFromArg);
|
||||
|
||||
@@ -70,10 +70,9 @@ function logSuccess(platform: RequestedPlatform) {
|
||||
|
||||
- Run ${chalk.bold('eas build')} when you are ready to create your first build.
|
||||
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to ${storesText}
|
||||
- ${learnMore(
|
||||
'https://docs.expo.io/build/introduction',
|
||||
'Learn more about other capabilities of EAS Build'
|
||||
)}`);
|
||||
- ${learnMore('https://docs.expo.io/build/introduction', {
|
||||
learnMoreMessage: 'Learn more about other capabilities of EAS Build',
|
||||
})}`);
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
@@ -68,7 +69,8 @@ export default class Build extends Command {
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectId = await getProjectIdAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
@@ -76,7 +78,7 @@ export default class Build extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
|
||||
accountName,
|
||||
platform,
|
||||
@@ -98,6 +100,7 @@ export default class Build extends Command {
|
||||
const commandCtx = await createCommandContextAsync({
|
||||
requestedPlatform: platform,
|
||||
profile: flags.profile,
|
||||
exp,
|
||||
projectDir,
|
||||
projectId,
|
||||
trackingCtx,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
@@ -26,9 +27,10 @@ export default class BuildList extends Command {
|
||||
const { platform, status, limit = 10 } = this.parse(BuildList).flags;
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectId = await getProjectIdAsync(projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const projectName = await getProjectFullNameAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const projectName = await getProjectFullNameAsync(exp);
|
||||
|
||||
const spinner = ora().start('Fetching the build list for the project…');
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import { HTTPError } from 'got';
|
||||
import ora from 'ora';
|
||||
@@ -22,9 +23,10 @@ export default class BuildView extends Command {
|
||||
const { BUILD_ID: buildId } = this.parse(BuildView).args;
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectId = await getProjectIdAsync(projectDir);
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const projectName = await getProjectFullNameAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const projectName = await getProjectFullNameAsync(exp);
|
||||
|
||||
const spinner = ora().start('Fetching the build…');
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import { UpdateChannel } from '../../graphql/generated';
|
||||
import {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
@@ -23,20 +26,17 @@ async function createUpdateChannelOnAppAsync({
|
||||
appId: string;
|
||||
channelName: string;
|
||||
branchId: string;
|
||||
}): Promise<UpdateChannel> {
|
||||
}): Promise<CreateUpdateChannelOnAppMutation> {
|
||||
// Point the new channel at a branch with its same name.
|
||||
const branchMapping = JSON.stringify({
|
||||
data: [{ branchId, branchMappingLogic: 'true' }],
|
||||
version: 0,
|
||||
});
|
||||
const data = await withErrorHandlingAsync(
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ updateChannel: { createUpdateChannelForApp: UpdateChannel } },
|
||||
{ appId: string; name: string; branchMapping: string }
|
||||
>(
|
||||
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateChannelForApp($appId: ID!, $name: String!, $branchMapping: String!) {
|
||||
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
|
||||
updateChannel {
|
||||
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
|
||||
id
|
||||
@@ -54,7 +54,6 @@ async function createUpdateChannelOnAppAsync({
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateChannel.createUpdateChannelForApp;
|
||||
}
|
||||
|
||||
export default class ChannelCreate extends Command {
|
||||
@@ -87,10 +86,9 @@ export default class ChannelCreate extends Command {
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const {
|
||||
exp: { slug },
|
||||
} = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
@@ -127,14 +125,22 @@ export default class ChannelCreate extends Command {
|
||||
branchMessage = `We also went ahead and made a branch with the same name`;
|
||||
}
|
||||
|
||||
const newChannel = await createUpdateChannelOnAppAsync({
|
||||
const {
|
||||
updateChannel: { createUpdateChannelForApp: newChannel },
|
||||
} = await createUpdateChannelOnAppAsync({
|
||||
appId: projectId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
|
||||
if (!newChannel) {
|
||||
throw new Error(
|
||||
`Could not create channel with name ${channelName} on app with id ${projectId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(newChannel);
|
||||
Log.log(JSON.stringify(newChannel));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetChannelByNameToEditQuery,
|
||||
GetChannelByNameToEditQueryVariables,
|
||||
UpdateBranch,
|
||||
UpdateChannel,
|
||||
UpdateChannelBranchMappingMutation,
|
||||
UpdateChannelBranchMappingMutationVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getBranchByNameAsync,
|
||||
getProjectAccountNameAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
async function getChannelByNameForAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
}: GetChannelByNameToEditQueryVariables): Promise<
|
||||
Pick<UpdateChannel, 'id' | 'name'> & {
|
||||
updateBranches: Pick<UpdateBranch, 'id' | 'name'>[];
|
||||
}
|
||||
> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetChannelByNameToEditQuery, GetChannelByNameToEditQueryVariables>(
|
||||
gql`
|
||||
query GetChannelByNameToEdit($appId: String!, $channelName: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannelByName(name: $channelName) {
|
||||
id
|
||||
name
|
||||
updateBranches(offset: 0, limit: 1) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, channelName }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const updateChannelByNameResult = data.app?.byId.updateChannelByName;
|
||||
if (!updateChannelByNameResult) {
|
||||
throw new Error(`Could not find a channel named ${channelName} on app with id ${appId}`);
|
||||
}
|
||||
return updateChannelByNameResult;
|
||||
}
|
||||
|
||||
async function updateChannelBranchMappingAsync({
|
||||
channelId,
|
||||
branchMapping,
|
||||
}: UpdateChannelBranchMappingMutationVariables): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
branchMapping: string;
|
||||
}> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<UpdateChannelBranchMappingMutation, UpdateChannelBranchMappingMutationVariables>(
|
||||
gql`
|
||||
mutation UpdateChannelBranchMapping($channelId: ID!, $branchMapping: String!) {
|
||||
updateChannel {
|
||||
editUpdateChannel(channelId: $channelId, branchMapping: $branchMapping) {
|
||||
id
|
||||
name
|
||||
branchMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ channelId, branchMapping }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const channel = data.updateChannel.editUpdateChannel;
|
||||
if (!channel) {
|
||||
throw new Error(`Could not fine channel with id ${channelId}`);
|
||||
}
|
||||
return data.updateChannel.editUpdateChannel!;
|
||||
}
|
||||
|
||||
export default class ChannelEdit extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Point a channel at a new branch.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: false,
|
||||
description: 'Name of the channel to edit',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
branch: flags.string({
|
||||
description: 'Name of the branch to point to',
|
||||
}),
|
||||
json: flags.boolean({
|
||||
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
let {
|
||||
args: { name: channelName },
|
||||
flags: { branch: branchName, json: jsonFlag },
|
||||
} = this.parse(ChannelEdit);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to edit a specific channel.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: channelName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please enter the name of the channel to edit:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const existingChannel = await getChannelByNameForAppAsync({ appId: projectId, channelName });
|
||||
// todo: refactor when multiple branches per channel are available
|
||||
const existingBranch = existingChannel.updateBranches[0];
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk`Channel {bold ${existingChannel.name}} is currently set to branch {bold ${existingBranch.name}}`
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
if (!branchName) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: chalk`What branch should it change to?`,
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const branch = await getBranchByNameAsync({ appId: projectId, name: branchName! });
|
||||
|
||||
const channel = await updateChannelBranchMappingAsync({
|
||||
channelId: existingChannel.id,
|
||||
// todo: move branch mapping logic to utility
|
||||
branchMapping: JSON.stringify({
|
||||
data: [{ branchId: branch.id, branchMappingLogic: 'true' }],
|
||||
version: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(channel));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
chalk`Channel {bold ${channel.name}} is now set to branch {bold ${branch.name}}.\n`
|
||||
);
|
||||
Log.log(
|
||||
chalk`Users with builds on channel {bold ${channel.name}} will now receive the active update on {bold ${branch.name}}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetAllChannelsForAppQuery,
|
||||
GetAllChannelsForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
|
||||
const CHANNEL_LIMIT = 10_000;
|
||||
|
||||
async function getAllUpdateChannelForAppAsync({
|
||||
appId,
|
||||
}: {
|
||||
appId: string;
|
||||
}): Promise<GetAllChannelsForAppQuery> {
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetAllChannelsForAppQuery, GetAllChannelsForAppQueryVariables>(
|
||||
gql`
|
||||
query GetAllChannelsForApp($appId: String!, $offset: Int!, $limit: Int!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannels(offset: $offset, limit: $limit) {
|
||||
id
|
||||
name
|
||||
updateBranches(offset: 0, limit: 1) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
id
|
||||
group
|
||||
message
|
||||
createdAt
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, offset: 0, limit: CHANNEL_LIMIT }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class ChannelList extends Command {
|
||||
static hidden = true;
|
||||
static description = 'List all channels on the current project.';
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
const {
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(ChannelList);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
|
||||
const getAllUpdateChannelForAppResult = await getAllUpdateChannelForAppAsync({
|
||||
appId: projectId,
|
||||
});
|
||||
const channels = getAllUpdateChannelForAppResult.app?.byId.updateChannels;
|
||||
if (!channels) {
|
||||
throw new Error(`Could not find channels on project with id ${projectId}`);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(channels));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: ['channel', 'branch', 'update', 'message', 'created-at', 'actor'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const channel of channels) {
|
||||
// TODO (cedric): refactor when multiple branches per channel are available
|
||||
const branch = channel.updateBranches[0];
|
||||
const update = branch.updates[0];
|
||||
|
||||
table.push([
|
||||
channel.name,
|
||||
branch.name,
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Channels for this app:}`);
|
||||
Log.log(table.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
GetChannelByNameForAppQuery,
|
||||
GetChannelByNameForAppQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
async function getUpdateChannelByNameForAppAsync({
|
||||
appId,
|
||||
channelName,
|
||||
}: GetChannelByNameForAppQueryVariables): Promise<GetChannelByNameForAppQuery> {
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetChannelByNameForAppQuery, GetChannelByNameForAppQueryVariables>(
|
||||
gql`
|
||||
query GetChannelByNameForApp($appId: String!, $channelName: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateChannelByName(name: $channelName) {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
updateBranches(offset: 0, limit: 1) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: 1) {
|
||||
id
|
||||
group
|
||||
message
|
||||
createdAt
|
||||
actor {
|
||||
id
|
||||
... on User {
|
||||
firstName
|
||||
}
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, channelName }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export default class ChannelView extends Command {
|
||||
static hidden = true;
|
||||
static description = 'View a channel on the current project.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: false,
|
||||
description: 'Name of the channel to view',
|
||||
},
|
||||
];
|
||||
|
||||
static flags = {
|
||||
json: flags.boolean({
|
||||
description: 'print output as a JSON object with the channel ID, name and branch mapping.',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async run() {
|
||||
let {
|
||||
args: { name: channelName },
|
||||
flags: { json: jsonFlag },
|
||||
} = this.parse(ChannelView);
|
||||
|
||||
const projectDir = await findProjectRootAsync(process.cwd());
|
||||
if (!projectDir) {
|
||||
throw new Error('Please run this command inside a project directory.');
|
||||
}
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
|
||||
if (!channelName) {
|
||||
const validationMessage = 'A channel name is required to view a specific channel.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name: channelName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'Please name the channel:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
const getUpdateChannelByNameForAppresult = await getUpdateChannelByNameForAppAsync({
|
||||
appId: projectId,
|
||||
channelName,
|
||||
});
|
||||
const channel = getUpdateChannelByNameForAppresult.app?.byId.updateChannelByName;
|
||||
if (!channel) {
|
||||
throw new Error(`Could not fine channel with name ${channelName}`);
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
Log.log(JSON.stringify(channel));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: ['branch', 'update', 'message', 'created-at', 'actor'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const branch of channel.updateBranches) {
|
||||
// TODO (cedric): refactor when multiple branches per channel are available
|
||||
const update = branch.updates[0];
|
||||
|
||||
table.push([
|
||||
branch.name,
|
||||
update?.group,
|
||||
update?.message,
|
||||
update?.createdAt && new Date(update.createdAt).toLocaleString(),
|
||||
update?.actor?.firstName,
|
||||
]);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
chalk`Channel: {bold ${channel.name}} on project {bold ${accountName}/${slug}}. Channel ID: {bold ${channel.id}}`
|
||||
);
|
||||
Log.log(chalk`{bold Recent update groups published on this branch:}`);
|
||||
Log.log(table.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
@@ -21,7 +22,8 @@ export default class BuildList extends Command {
|
||||
let appleTeamIdentifier = this.parse(BuildList).flags['apple-team-id'];
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
let spinner: ora.Ora;
|
||||
|
||||
@@ -42,7 +44,9 @@ export default class BuildList extends Command {
|
||||
name: 'appleTeamIdentifier',
|
||||
message: 'What Apple Team would you like to list devices for?',
|
||||
choices: teams.map(team => ({
|
||||
title: `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`,
|
||||
title: team.appleTeamName
|
||||
? `${team.appleTeamName} (ID: ${team.appleTeamIdentifier})`
|
||||
: team.appleTeamIdentifier,
|
||||
value: team.appleTeamIdentifier,
|
||||
})),
|
||||
});
|
||||
@@ -71,7 +75,9 @@ export default class BuildList extends Command {
|
||||
if (result?.appleDevices.length) {
|
||||
const { appleTeamName, appleDevices } = result;
|
||||
|
||||
spinner.succeed(`Found ${appleDevices.length} devices for team ${appleTeamName}`);
|
||||
spinner.succeed(
|
||||
`Found ${appleDevices.length} devices for team ${appleTeamName ?? appleTeamIdentifier}`
|
||||
);
|
||||
|
||||
const list = appleDevices
|
||||
.map(device =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
|
||||
@@ -29,7 +30,8 @@ If you are not sure what is the UDID of the device you are looking for, run:
|
||||
}
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
const spinner = ora().start(`Fetching device details for ${UDID}…`);
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import Log from '../../log';
|
||||
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { ensureLoggedInAsync, getActorDisplayName } from '../../user/actions';
|
||||
|
||||
export enum EnvironmentSecretTargetLocation {
|
||||
ACCOUNT = 'account',
|
||||
PROJECT = 'project',
|
||||
}
|
||||
export default class EnvironmentSecretCreate extends Command {
|
||||
static hidden = true;
|
||||
static description = 'Create an environment secret on the current project or owner account.';
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'target',
|
||||
required: false,
|
||||
description: 'Target location for the secret',
|
||||
options: [EnvironmentSecretTargetLocation.ACCOUNT, EnvironmentSecretTargetLocation.PROJECT],
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
required: false,
|
||||
description: 'Name of the secret',
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: false,
|
||||
description: 'Value of the secret',
|
||||
},
|
||||
];
|
||||
|
||||
async run() {
|
||||
const user = await ensureLoggedInAsync();
|
||||
let {
|
||||
args: { name, value: secretValue, target },
|
||||
} = this.parse(EnvironmentSecretCreate);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
const { slug } = exp;
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName,
|
||||
projectName: slug,
|
||||
});
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
const validationMessage = 'Secret target may not be empty.';
|
||||
|
||||
({ target } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'target',
|
||||
message: 'Where should this secret be used:',
|
||||
choices: [
|
||||
{ title: 'Account-wide', value: EnvironmentSecretTargetLocation.ACCOUNT },
|
||||
{ title: 'Project-specific', value: EnvironmentSecretTargetLocation.PROJECT },
|
||||
],
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
({ name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `Secret name:`,
|
||||
validate: value => {
|
||||
if (!value) {
|
||||
return 'Secret name may not be empty.';
|
||||
}
|
||||
|
||||
if (value.match(/^\w+$/)) {
|
||||
return 'Names may contain only letters, numbers, and underscores.';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (!secretValue) {
|
||||
const validationMessage = 'Secret value may not be empty.';
|
||||
|
||||
({ secretValue } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'secretValue',
|
||||
message: 'Secret value:',
|
||||
validate: value => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
|
||||
if (target === EnvironmentSecretTargetLocation.PROJECT) {
|
||||
const secret = await EnvironmentSecretMutation.createForApp(
|
||||
{ name, value: secretValue },
|
||||
projectId
|
||||
);
|
||||
if (!secret) {
|
||||
throw new Error(
|
||||
`Could not create secret with name ${name} on project with id ${projectId}`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} on project ${chalk.bold(
|
||||
`@${accountName}/${slug}`
|
||||
)}.`
|
||||
);
|
||||
} else if (target === EnvironmentSecretTargetLocation.ACCOUNT) {
|
||||
const ownerAccount = findAccountByName(user.accounts, accountName);
|
||||
if (!ownerAccount) {
|
||||
Log.warn(
|
||||
`Your account (${getActorDisplayName(user)}) doesn't have access to the ${chalk.bold(
|
||||
accountName
|
||||
)} account`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const secret = await EnvironmentSecretMutation.createForAccount(
|
||||
{ name, value: secretValue },
|
||||
ownerAccount.id
|
||||
);
|
||||
|
||||
if (!secret) {
|
||||
throw new Error(
|
||||
`Could not create secret with name ${name} on account with id ${ownerAccount.id}`
|
||||
);
|
||||
}
|
||||
|
||||
Log.withTick(
|
||||
`️Created a new secret ${chalk.bold(name)} on account ${chalk.bold(ownerAccount.name)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class EnvironmentSecretDelete extends Command {
|
||||
static hidden = true;
|
||||
static description = `
|
||||
Delete an environment secret by ID.
|
||||
Unsure where to find the secret's ID? Run ${'`eas secrets:list`'}
|
||||
`;
|
||||
|
||||
static args = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: `ID of the secret to delete`,
|
||||
},
|
||||
];
|
||||
|
||||
async run() {
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
args: { id },
|
||||
} = this.parse(EnvironmentSecretDelete);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permamently delete secret with id: "${id}".\nThis action is irreversable.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error(`Canceled deletion of secret with id: "${id}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await EnvironmentSecretMutation.delete(id);
|
||||
|
||||
Log.withTick(`️Deleted secret with id "${id}".`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import { EnvironmentSecretFragment } from '../../graphql/generated';
|
||||
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { EnvironmentSecretTargetLocation } from './create';
|
||||
|
||||
export default class EnvironmentSecretsList extends Command {
|
||||
static description = 'Lists environment secrets available for your current app';
|
||||
static usage = 'secrets:list';
|
||||
|
||||
async run(): Promise<void> {
|
||||
await ensureLoggedInAsync();
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
const projectFullName = await getProjectFullNameAsync(exp);
|
||||
const projectAccountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!projectDir) {
|
||||
throw new Error("Please run this command inside your project's directory");
|
||||
}
|
||||
|
||||
const [accountSecrets, appSecrets] = await Promise.all([
|
||||
EnvironmentSecretsQuery.byAcccountNameAsync(projectAccountName),
|
||||
EnvironmentSecretsQuery.byAppFullNameAsync(projectFullName),
|
||||
]);
|
||||
|
||||
const secrets = [
|
||||
...appSecrets.map(s => ({ ...s, target: EnvironmentSecretTargetLocation.PROJECT })),
|
||||
...accountSecrets.map(s => ({ ...s, target: EnvironmentSecretTargetLocation.ACCOUNT })),
|
||||
] as (EnvironmentSecretFragment & { target: EnvironmentSecretTargetLocation })[];
|
||||
|
||||
const table = new Table({
|
||||
head: ['Name', 'Target', 'Updated at', 'ID'],
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const secret of secrets) {
|
||||
const { name, createdAt: updatedAt, target, id } = secret;
|
||||
table.push([name, target, dateFormat(updatedAt, 'mmm dd HH:MM:ss'), id]);
|
||||
}
|
||||
|
||||
Log.log(chalk`{bold Secrets for this account and project:}`);
|
||||
Log.log(table.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import { learnMore } from '../log';
|
||||
@@ -168,7 +169,8 @@ export default class BuildSubmit extends Command {
|
||||
const platform = platformFromParams ?? (await promptForPlatformAsync());
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const projectId = await getProjectIdAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const projectId = await getProjectIdAsync(exp);
|
||||
|
||||
if (!(await isEasEnabledForProjectAsync(projectId))) {
|
||||
warnEasUnavailable();
|
||||
|
||||
@@ -24,7 +24,7 @@ interface Options {
|
||||
}
|
||||
|
||||
export default class AndroidCredentialsProvider implements CredentialsProvider {
|
||||
public readonly platform = Platform.Android;
|
||||
public readonly platform = Platform.ANDROID;
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, iOSDistributionType } from '@expo/eas-json';
|
||||
|
||||
import { IosDistributionType } from '../../graphql/generated';
|
||||
import { AppleTeamFragment, IosDistributionType } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import { CredentialsManager } from '../CredentialsManager';
|
||||
@@ -10,6 +10,9 @@ import { Context } from '../context';
|
||||
import * as credentialsJsonReader from '../credentialsJson/read';
|
||||
import type { IosCredentials } from '../credentialsJson/read';
|
||||
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './actions/new/AppleTeamUtils';
|
||||
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from './errors';
|
||||
import { isAdHocProfile } from './utils/provisioningProfile';
|
||||
|
||||
export { IosCredentials };
|
||||
|
||||
@@ -34,11 +37,16 @@ interface AppLookupParams {
|
||||
}
|
||||
|
||||
export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
public readonly platform = Platform.iOS;
|
||||
public readonly platform = Platform.IOS;
|
||||
|
||||
constructor(private ctx: Context, private options: Options) {}
|
||||
|
||||
public async hasRemoteAsync(): Promise<boolean> {
|
||||
// TODO: this is temporary
|
||||
// remove this check when we implement syncing local credentials for internal distribution
|
||||
if (this.options.distribution === 'internal' && (await this.hasLocalAsync())) {
|
||||
return false;
|
||||
}
|
||||
const { distributionCertificate, provisioningProfile } = await this.fetchRemoteAsync();
|
||||
return !!(
|
||||
distributionCertificate?.certP12 ||
|
||||
@@ -48,10 +56,6 @@ export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
}
|
||||
|
||||
public async hasLocalAsync(): Promise<boolean> {
|
||||
if (this.options.distribution === 'internal') {
|
||||
// TODO: add support for using credentials.json for internal distribution
|
||||
return false;
|
||||
}
|
||||
if (!(await credentialsJsonReader.fileExistsAsync(this.ctx.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
@@ -102,11 +106,32 @@ export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
}
|
||||
|
||||
private async getLocalAsync(): Promise<IosCredentials> {
|
||||
if (this.options.distribution === 'internal') {
|
||||
// TODO: add support for using credentials.json for internal distribution
|
||||
throw new Error('Using credentials.json for internal distribution is not supported yet.');
|
||||
const credentials = await credentialsJsonReader.readIosCredentialsAsync(this.ctx.projectDir);
|
||||
if (credentialsJsonReader.isCredentialsMap(credentials)) {
|
||||
for (const targetName of Object.keys(credentials)) {
|
||||
this.assertProvisioningProfileType(credentials[targetName].provisioningProfile, targetName);
|
||||
}
|
||||
} else {
|
||||
this.assertProvisioningProfileType(credentials.provisioningProfile);
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private assertProvisioningProfileType(provisionigProfile: string, targetName?: string) {
|
||||
const isAdHoc = isAdHocProfile(provisionigProfile);
|
||||
if (this.options.distribution === 'internal' && !isAdHoc) {
|
||||
throw new Error(
|
||||
`You must use an adhoc provisioning profile${
|
||||
targetName && ` (target '${targetName})'`
|
||||
} for internal distribution`
|
||||
);
|
||||
} else if (this.options.distribution !== 'internal' && isAdHoc) {
|
||||
throw new Error(
|
||||
`You can't use an adhoc provisioning profile${
|
||||
targetName && ` (target '${targetName}')`
|
||||
} for app store distribution`
|
||||
);
|
||||
}
|
||||
return await credentialsJsonReader.readIosCredentialsAsync(this.ctx.projectDir);
|
||||
}
|
||||
|
||||
private async getRemoteAsync(): Promise<IosCredentials> {
|
||||
@@ -160,31 +185,36 @@ export default class IosCredentialsProvider implements CredentialsProvider {
|
||||
projectName: this.options.app.projectName,
|
||||
};
|
||||
|
||||
// for now, let's require the user to authenticate with Apple
|
||||
const { team } = await this.ctx.appStore.ensureAuthenticatedAsync();
|
||||
const appleTeam = await this.ctx.newIos.createOrGetExistingAppleTeamAsync(appLookupParams, {
|
||||
appleTeamIdentifier: team.id,
|
||||
appleTeamName: team.name,
|
||||
});
|
||||
const [distCert, provisioningProfile] = await Promise.all([
|
||||
this.ctx.newIos.getDistributionCertificateForAppAsync(
|
||||
appLookupParams,
|
||||
appleTeam,
|
||||
IosDistributionType.AdHoc
|
||||
),
|
||||
this.ctx.newIos.getProvisioningProfileAsync(
|
||||
appLookupParams,
|
||||
appleTeam,
|
||||
IosDistributionType.AdHoc
|
||||
),
|
||||
]);
|
||||
return {
|
||||
provisioningProfile: provisioningProfile?.provisioningProfile ?? undefined,
|
||||
distributionCertificate: {
|
||||
certP12: distCert?.certificateP12 ?? undefined,
|
||||
certPassword: distCert?.certificatePassword ?? undefined,
|
||||
},
|
||||
};
|
||||
let appleTeam: AppleTeamFragment | null = null;
|
||||
if (!this.ctx.nonInteractive) {
|
||||
await this.ctx.appStore.ensureAuthenticatedAsync();
|
||||
appleTeam = await resolveAppleTeamIfAuthenticatedAsync(this.ctx, appLookupParams);
|
||||
}
|
||||
|
||||
try {
|
||||
const [distCert, provisioningProfile] = await Promise.all([
|
||||
this.ctx.newIos.getDistributionCertificateForAppAsync(
|
||||
appLookupParams,
|
||||
IosDistributionType.AdHoc,
|
||||
{ appleTeam }
|
||||
),
|
||||
this.ctx.newIos.getProvisioningProfileAsync(appLookupParams, IosDistributionType.AdHoc, {
|
||||
appleTeam,
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
provisioningProfile: provisioningProfile?.provisioningProfile ?? undefined,
|
||||
distributionCertificate: {
|
||||
certP12: distCert?.certificateP12 ?? undefined,
|
||||
certPassword: distCert?.certificatePassword ?? undefined,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof AppleTeamMissingError && this.ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
const [distCert, provisioningProfile] = await Promise.all([
|
||||
this.ctx.ios.getDistributionCertificateAsync(this.options.app),
|
||||
|
||||
@@ -37,11 +37,6 @@ export class SetupBuildCredentials implements Action {
|
||||
if (!account) {
|
||||
throw new Error(`You do not have access to the ${this.app.accountName} account`);
|
||||
}
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error('Internal distribution builds are not supported in non-interactive mode');
|
||||
}
|
||||
// for now, let's require the user to authenticate with Apple
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const action = new SetupAdhocProvisioningProfile({
|
||||
account,
|
||||
projectName: this.app.projectName,
|
||||
|
||||
@@ -11,7 +11,11 @@ export class UpdateCredentialsJson implements Action {
|
||||
constructor(private app: AppLookupParams) {}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const bundleIdentifer = await ensureAppIdentifierIsDefinedAsync(ctx.projectDir, Platform.iOS);
|
||||
const bundleIdentifer = await ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
platform: Platform.IOS,
|
||||
exp: ctx.exp,
|
||||
});
|
||||
Log.log('Updating iOS credentials in credentials.json');
|
||||
await updateIosCredentialsAsync(ctx, bundleIdentifer);
|
||||
Log.succeed(
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppleTeamFragment } from '../../../../graphql/generated';
|
||||
import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
|
||||
export async function resolveAppleTeamIfAuthenticatedAsync(
|
||||
ctx: Context,
|
||||
app: AppLookupParams
|
||||
): Promise<AppleTeamFragment | null> {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
return null;
|
||||
}
|
||||
return await ctx.newIos.createOrGetExistingAppleTeamAsync(app, {
|
||||
appleTeamIdentifier: ctx.appStore.authCtx.team.id,
|
||||
appleTeamName: ctx.appStore.authCtx.team.name,
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../../graphql/generated';
|
||||
import Log from '../../../../log';
|
||||
import { confirmAsync } from '../../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../../CredentialsManager';
|
||||
import { Context } from '../../../context';
|
||||
@@ -15,6 +16,8 @@ import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/Appl
|
||||
import { AppleProvisioningProfileQueryResult } from '../../api/graphql/queries/AppleProvisioningProfileQuery';
|
||||
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
|
||||
import { ProfileClass } from '../../appstore/provisioningProfile';
|
||||
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { chooseDevices } from './DeviceUtils';
|
||||
import { doUDIDsMatch, isDevPortalAdhocProfileValid } from './ProvisioningProfileUtils';
|
||||
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
|
||||
@@ -33,15 +36,55 @@ export class SetupAdhocProvisioningProfile implements Action {
|
||||
}
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
const authCtx = await ctx.appStore.ensureAuthenticatedAsync();
|
||||
if (ctx.nonInteractive) {
|
||||
try {
|
||||
await this.runNonInteractiveAsync(manager, ctx);
|
||||
} catch (err) {
|
||||
if (err instanceof AppleTeamMissingError) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
await this.runInteractiveAsync(manager, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// 0. Fetch apple team object
|
||||
const appleTeam = await ctx.newIos.createOrGetExistingAppleTeamAsync(this.app, {
|
||||
appleTeamIdentifier: authCtx.team.id,
|
||||
appleTeamName: authCtx.team.name,
|
||||
});
|
||||
private async runNonInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
// 1. Setup Distribution Certificate
|
||||
const distCertAction = new SetupDistributionCertificate(this.app);
|
||||
await manager.runActionAsync(distCertAction);
|
||||
|
||||
// 1. Fetch devices registered on EAS servers.
|
||||
// 2. Fetch profile from EAS servers
|
||||
const currentProfile = await ctx.newIos.getProvisioningProfileAsync(
|
||||
this.app,
|
||||
IosDistributionType.AdHoc
|
||||
);
|
||||
|
||||
if (!currentProfile) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
|
||||
// TODO: implement validation
|
||||
Log.warn(
|
||||
'Provisioning Profile is not validated for non-interactive internal distribution builds.'
|
||||
);
|
||||
|
||||
// app credentials should exist here because the profile exists
|
||||
const appCredentials = nullthrows(
|
||||
await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(this.app, {
|
||||
iosDistributionType: IosDistributionType.AdHoc,
|
||||
})
|
||||
);
|
||||
this._iosAppBuildCredentials = appCredentials.iosAppBuildCredentialsArray[0];
|
||||
}
|
||||
|
||||
private async runInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
// 0. Ensure the user is authenticated with Apple and resolve the Apple team object
|
||||
await ctx.appStore.ensureAuthenticatedAsync();
|
||||
const appleTeam = nullthrows(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app));
|
||||
|
||||
// 1. Fetch devices registered on EAS servers
|
||||
const registeredAppleDevices = await ctx.newIos.getDevicesForAppleTeamAsync(
|
||||
this.app,
|
||||
appleTeam
|
||||
@@ -58,17 +101,17 @@ export class SetupAdhocProvisioningProfile implements Action {
|
||||
await manager.runActionAsync(distCertAction);
|
||||
const distCert = distCertAction.distributionCertificate;
|
||||
|
||||
// 3. Fetch profile from EAS servers
|
||||
let currentProfileFromExpoServers = (await ctx.newIos.getProvisioningProfileAsync(
|
||||
this.app,
|
||||
let profileFromExpoServersToUse:
|
||||
| AppleProvisioningProfileQueryResult
|
||||
| AppleProvisioningProfileMutationResult
|
||||
| null = await ctx.newIos.getProvisioningProfileAsync(this.app, IosDistributionType.AdHoc, {
|
||||
appleTeam,
|
||||
IosDistributionType.AdHoc
|
||||
)) as AppleProvisioningProfileQueryResult | AppleProvisioningProfileMutationResult | null;
|
||||
});
|
||||
|
||||
// 4. Choose devices for internal distribution
|
||||
let chosenDevices: AppleDeviceFragmentWithAppleTeam[];
|
||||
if (currentProfileFromExpoServers) {
|
||||
const appleDeviceIdentifiersFromProfile = ((currentProfileFromExpoServers.appleDevices ??
|
||||
if (profileFromExpoServersToUse) {
|
||||
const appleDeviceIdentifiersFromProfile = ((profileFromExpoServersToUse.appleDevices ??
|
||||
[]) as AppleDevice[]).map(({ identifier }) => identifier);
|
||||
|
||||
let shouldAskToChooseDevices = true;
|
||||
@@ -110,10 +153,10 @@ export class SetupAdhocProvisioningProfile implements Action {
|
||||
this.app,
|
||||
appleTeam
|
||||
);
|
||||
if (currentProfileFromExpoServers) {
|
||||
if (profileFromExpoServersToUse) {
|
||||
if (profileWasRecreated) {
|
||||
currentProfileFromExpoServers = await ctx.newIos.updateProvisioningProfileAsync(
|
||||
currentProfileFromExpoServers.id,
|
||||
profileFromExpoServersToUse = await ctx.newIos.updateProvisioningProfileAsync(
|
||||
profileFromExpoServersToUse.id,
|
||||
{
|
||||
appleProvisioningProfile: profileToUse.provisioningProfile,
|
||||
developerPortalIdentifier: profileToUse.provisioningProfileId,
|
||||
@@ -121,7 +164,7 @@ export class SetupAdhocProvisioningProfile implements Action {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
currentProfileFromExpoServers = await ctx.newIos.createProvisioningProfileAsync(
|
||||
profileFromExpoServersToUse = await ctx.newIos.createProvisioningProfileAsync(
|
||||
this.app,
|
||||
appleAppIdentifier,
|
||||
{
|
||||
@@ -138,7 +181,7 @@ export class SetupAdhocProvisioningProfile implements Action {
|
||||
appleTeam,
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
appleDistributionCertificateId: distCert.id,
|
||||
appleProvisioningProfileId: currentProfileFromExpoServers.id,
|
||||
appleProvisioningProfileId: profileFromExpoServersToUse.id,
|
||||
iosDistributionType: IosDistributionType.AdHoc,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ import { Context } from '../../../context';
|
||||
import { AppLookupParams } from '../../api/GraphqlClient';
|
||||
import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation';
|
||||
import { getValidCertSerialNumbers } from '../../appstore/CredentialsUtils';
|
||||
import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
|
||||
import { formatDistributionCertificate } from './DistributionCertificateUtils';
|
||||
|
||||
@@ -31,21 +33,48 @@ export class SetupDistributionCertificate implements Action {
|
||||
}
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
assert(
|
||||
ctx.appStore.authCtx,
|
||||
'authCtx is defined in this context - enforced by ensureAuthenticatedAsync call in SetupBuildCredentials'
|
||||
);
|
||||
const appleTeam = await ctx.newIos.createOrGetExistingAppleTeamAsync(this.app, {
|
||||
appleTeamIdentifier: ctx.appStore.authCtx.team.id,
|
||||
appleTeamName: ctx.appStore.authCtx.team.name,
|
||||
});
|
||||
const appleTeam = await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app);
|
||||
|
||||
const currentCertificate = await ctx.newIos.getDistributionCertificateForAppAsync(
|
||||
this.app,
|
||||
appleTeam,
|
||||
IosDistributionType.AdHoc
|
||||
);
|
||||
try {
|
||||
const currentCertificate = await ctx.newIos.getDistributionCertificateForAppAsync(
|
||||
this.app,
|
||||
IosDistributionType.AdHoc,
|
||||
{ appleTeam }
|
||||
);
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
await this.runNonInteractiveAsync(ctx, currentCertificate);
|
||||
} else {
|
||||
await this.runInteractiveAsync(ctx, manager, currentCertificate);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof AppleTeamMissingError && ctx.nonInteractive) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async runNonInteractiveAsync(
|
||||
ctx: Context,
|
||||
currentCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<void> {
|
||||
// TODO: implement validation
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
'Distribution Certificate is not validated for non-interactive internal distribution builds.'
|
||||
);
|
||||
if (!currentCertificate) {
|
||||
throw new MissingCredentialsNonInteractiveError();
|
||||
}
|
||||
this._distributionCertificate = currentCertificate;
|
||||
}
|
||||
|
||||
private async runInteractiveAsync(
|
||||
ctx: Context,
|
||||
manager: CredentialsManager,
|
||||
currentCertificate: AppleDistributionCertificateFragment | null
|
||||
): Promise<void> {
|
||||
if (await this.isCurrentCertificateValidAsync(ctx, currentCertificate)) {
|
||||
assert(currentCertificate, 'currentCertificate is defined here');
|
||||
this._distributionCertificate = currentCertificate;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../../../graphql/generated';
|
||||
import { Account } from '../../../user/Account';
|
||||
import { DistributionCertificate } from '../appstore/Credentials.types';
|
||||
import { AppleTeamMissingError } from '../errors';
|
||||
import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation';
|
||||
import {
|
||||
AppleDistributionCertificateMutation,
|
||||
@@ -98,6 +99,25 @@ export async function createOrUpdateIosAppBuildCredentialsAsync(
|
||||
}
|
||||
|
||||
export async function getIosAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
{ iosDistributionType }: { iosDistributionType: IosDistributionType }
|
||||
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult | null> {
|
||||
const { account, bundleIdentifier } = appLookupParams;
|
||||
const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync(
|
||||
account.name,
|
||||
bundleIdentifier
|
||||
);
|
||||
if (!appleAppIdentifier) {
|
||||
return null;
|
||||
}
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(projectFullName, {
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
iosDistributionType,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getIosAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams: AppLookupParams
|
||||
): Promise<CommonIosAppCredentialsFragment | null> {
|
||||
const { account, bundleIdentifier } = appLookupParams;
|
||||
@@ -126,12 +146,12 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs
|
||||
iosDistributionType: IosDistributionType;
|
||||
}
|
||||
): Promise<IosAppCredentialsWithBuildCredentialsQueryResult> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
const maybeIosAppCredentials = await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(
|
||||
projectFullName,
|
||||
{ appleAppIdentifierId, iosDistributionType }
|
||||
const maybeIosAppCredentials = await getIosAppCredentialsWithBuildCredentialsAsync(
|
||||
appLookupParams,
|
||||
{
|
||||
iosDistributionType,
|
||||
}
|
||||
);
|
||||
|
||||
if (maybeIosAppCredentials) {
|
||||
return maybeIosAppCredentials;
|
||||
} else {
|
||||
@@ -144,6 +164,7 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs
|
||||
app.id,
|
||||
appleAppIdentifier.id
|
||||
);
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
return nullthrows(
|
||||
await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(projectFullName, {
|
||||
appleAppIdentifierId,
|
||||
@@ -173,7 +194,7 @@ export async function createOrGetExistingAppleTeamAsync(
|
||||
|
||||
export async function createOrGetExistingAppleAppIdentifierAsync(
|
||||
{ account, bundleIdentifier }: AppLookupParams,
|
||||
appleTeam: AppleTeamFragment
|
||||
appleTeam: AppleTeamFragment | null
|
||||
): Promise<AppleAppIdentifierFragment> {
|
||||
const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync(
|
||||
account.name,
|
||||
@@ -182,6 +203,9 @@ export async function createOrGetExistingAppleAppIdentifierAsync(
|
||||
if (appleAppIdentifier) {
|
||||
return appleAppIdentifier;
|
||||
} else {
|
||||
if (!appleTeam) {
|
||||
throw new AppleTeamMissingError();
|
||||
}
|
||||
return await AppleAppIdentifierMutation.createAppleAppIdentifierAsync(
|
||||
{ bundleIdentifier, appleTeamId: appleTeam.id },
|
||||
account.id
|
||||
@@ -213,8 +237,8 @@ export async function createProvisioningProfileAsync(
|
||||
|
||||
export async function getProvisioningProfileAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
appleTeam: AppleTeamFragment,
|
||||
iosDistributionType: IosDistributionType
|
||||
iosDistributionType: IosDistributionType,
|
||||
{ appleTeam }: { appleTeam: AppleTeamFragment | null } = { appleTeam: null }
|
||||
): Promise<AppleProvisioningProfileQueryResult | null> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync(
|
||||
@@ -222,7 +246,7 @@ export async function getProvisioningProfileAsync(
|
||||
appleTeam
|
||||
);
|
||||
return await AppleProvisioningProfileQuery.getForAppAsync(projectFullName, {
|
||||
appleAppIdentifierId: appleAppIdentifier?.id,
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
iosDistributionType,
|
||||
});
|
||||
}
|
||||
@@ -250,8 +274,8 @@ export async function deleteProvisioningProfilesAsync(
|
||||
|
||||
export async function getDistributionCertificateForAppAsync(
|
||||
appLookupParams: AppLookupParams,
|
||||
appleTeam: AppleTeamFragment,
|
||||
iosDistributionType: IosDistributionType
|
||||
iosDistributionType: IosDistributionType,
|
||||
{ appleTeam }: { appleTeam: AppleTeamFragment | null } = { appleTeam: null }
|
||||
): Promise<AppleDistributionCertificateFragment | null> {
|
||||
const projectFullName = formatProjectFullName(appLookupParams);
|
||||
const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync(
|
||||
@@ -259,7 +283,7 @@ export async function getDistributionCertificateForAppAsync(
|
||||
appleTeam
|
||||
);
|
||||
return await AppleDistributionCertificateQuery.getForAppAsync(projectFullName, {
|
||||
appleAppIdentifierId: appleAppIdentifier?.id,
|
||||
appleAppIdentifierId: appleAppIdentifier.id,
|
||||
iosDistributionType,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export class AppleTeamMissingError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Apple Team is necessary to create Apple App Identifier');
|
||||
}
|
||||
}
|
||||
|
||||
export class MissingCredentialsNonInteractiveError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(
|
||||
message ?? 'Credentials are not set up. Please run this command again in interactive mode.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ function readProfileName(dataBase64: string): string {
|
||||
return profilePlist['Name'] as string;
|
||||
}
|
||||
|
||||
function isAdHocProfile(dataBase64: string): boolean {
|
||||
const profilePlist = parse(dataBase64);
|
||||
const provisionedDevices = profilePlist['ProvisionedDevices'] as string[] | undefined;
|
||||
return Array.isArray(provisionedDevices);
|
||||
}
|
||||
|
||||
function parse(dataBase64: string): PlistObject {
|
||||
try {
|
||||
const buffer = Buffer.from(dataBase64, 'base64');
|
||||
@@ -30,4 +36,4 @@ function parse(dataBase64: string): PlistObject {
|
||||
}
|
||||
}
|
||||
|
||||
export { readAppleTeam, readProfileName, parse };
|
||||
export { readAppleTeam, readProfileName, isAdHocProfile, parse };
|
||||
@@ -40,7 +40,7 @@ export class ManageIosBeta implements Action {
|
||||
}
|
||||
if (ctx.hasProjectContext) {
|
||||
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
|
||||
const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(
|
||||
const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithCommonFieldsAsync(
|
||||
appLookupParams
|
||||
);
|
||||
if (!iosAppCredentials) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
|
||||
import AppStoreApi from '../credentials/ios/appstore/AppStoreApi';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { Actor } from '../user/User';
|
||||
|
||||
export interface DeviceManagerContext {
|
||||
appStore: AppStoreApi;
|
||||
exp: ExpoConfig | null;
|
||||
projectDir: string | null;
|
||||
user: Actor;
|
||||
}
|
||||
@@ -17,9 +20,16 @@ export async function createContext({
|
||||
cwd?: string;
|
||||
user: Actor;
|
||||
}): Promise<DeviceManagerContext> {
|
||||
const projectDir = await findProjectRootAsync(cwd);
|
||||
let exp: ExpoConfig | null = null;
|
||||
if (projectDir) {
|
||||
const config = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
exp = config.exp;
|
||||
}
|
||||
return {
|
||||
appStore,
|
||||
projectDir: await findProjectRootAsync(cwd),
|
||||
projectDir,
|
||||
exp,
|
||||
user,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -39,16 +40,16 @@ export default class DeviceManager {
|
||||
}
|
||||
|
||||
private async resolveAccountAsync(): Promise<Account> {
|
||||
const resolver = new AccountResolver(this.ctx.projectDir, this.ctx.user);
|
||||
const resolver = new AccountResolver(this.ctx.exp, this.ctx.user);
|
||||
return await resolver.resolveAccountAsync();
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountResolver {
|
||||
constructor(private projectDir: string | null, private user: Actor) {}
|
||||
constructor(private exp: ExpoConfig | null, private user: Actor) {}
|
||||
|
||||
public async resolveAccountAsync(): Promise<Account> {
|
||||
if (this.projectDir) {
|
||||
if (this.exp) {
|
||||
const account = await this.resolveProjectAccountAsync();
|
||||
if (account) {
|
||||
return account;
|
||||
@@ -58,9 +59,9 @@ export class AccountResolver {
|
||||
}
|
||||
|
||||
private async resolveProjectAccountAsync(): Promise<Account | undefined> {
|
||||
assert(this.projectDir, 'project directory is not set ');
|
||||
assert(this.exp, 'expo config is not set');
|
||||
|
||||
const projectAccountName = await getProjectAccountNameAsync(this.projectDir);
|
||||
const projectAccountName = await getProjectAccountNameAsync(this.exp);
|
||||
const projectAccount = findAccountByName(this.user.accounts, projectAccountName);
|
||||
if (!projectAccount) {
|
||||
Log.warn(
|
||||
|
||||
@@ -31,6 +31,8 @@ export type RootQuery = {
|
||||
* @deprecated Not used.
|
||||
*/
|
||||
_doNotUse?: Maybe<Scalars['String']>;
|
||||
/** fetch all updates in a group */
|
||||
updatesByGroup: Array<Update>;
|
||||
/** Top-level query object for querying Accounts. */
|
||||
account: AccountQuery;
|
||||
/** Top-level query object for querying Actors. */
|
||||
@@ -88,6 +90,11 @@ export type RootQuery = {
|
||||
};
|
||||
|
||||
|
||||
export type RootQueryUpdatesByGroupArgs = {
|
||||
group: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type RootQueryAppByAppIdArgs = {
|
||||
appId: Scalars['String'];
|
||||
};
|
||||
@@ -126,24 +133,52 @@ export type RootQueryUserByUsernameArgs = {
|
||||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AccountQuery = {
|
||||
__typename?: 'AccountQuery';
|
||||
/** Query an Account by ID */
|
||||
byId: Account;
|
||||
/** Query an Account by name */
|
||||
byName: Account;
|
||||
export type Update = ActivityTimelineProjectActivity & {
|
||||
__typename?: 'Update';
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
branchId: Scalars['ID'];
|
||||
platform: Scalars['String'];
|
||||
manifestFragment: Scalars['String'];
|
||||
runtimeVersion: Scalars['String'];
|
||||
group: Scalars['String'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
message?: Maybe<Scalars['String']>;
|
||||
branch: UpdateBranch;
|
||||
};
|
||||
|
||||
export type ActivityTimelineProjectActivity = {
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
/** A user or robot that can authenticate with Expo services and be a member of accounts. */
|
||||
export type Actor = {
|
||||
id: Scalars['ID'];
|
||||
firstName?: Maybe<Scalars['String']>;
|
||||
created?: Maybe<Scalars['DateTime']>;
|
||||
isExpoAdmin?: Maybe<Scalars['Boolean']>;
|
||||
/** Associated accounts */
|
||||
accounts: Array<Account>;
|
||||
/** Access Tokens belonging to this actor */
|
||||
accessTokens: Array<AccessToken>;
|
||||
/**
|
||||
* Server feature gate values for this actor, optionally filtering by desired gates.
|
||||
* Only resolves for the viewer.
|
||||
*/
|
||||
featureGates: Scalars['JSONObject'];
|
||||
};
|
||||
|
||||
|
||||
export type AccountQueryByIdArgs = {
|
||||
accountId: Scalars['String'];
|
||||
/** A user or robot that can authenticate with Expo services and be a member of accounts. */
|
||||
export type ActorFeatureGatesArgs = {
|
||||
filter?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
|
||||
export type AccountQueryByNameArgs = {
|
||||
accountName: Scalars['String'];
|
||||
};
|
||||
|
||||
/**
|
||||
* An account is a container owning projects, credentials, billing and other organization
|
||||
* data and settings. Actors may own and be members of accounts.
|
||||
@@ -217,6 +252,7 @@ export type AccountSnacksArgs = {
|
||||
export type AccountAppsArgs = {
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
includeUnpublished?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -287,7 +323,6 @@ export type AccountEnvironmentSecretsArgs = {
|
||||
filterNames?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
|
||||
export type Offer = {
|
||||
__typename?: 'Offer';
|
||||
id: Scalars['ID'];
|
||||
@@ -575,6 +610,7 @@ export type UserSnacksArgs = {
|
||||
export type UserAppsArgs = {
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
includeUnpublished?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -590,43 +626,6 @@ export type UserFeatureGatesArgs = {
|
||||
filter?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
/** A user or robot that can authenticate with Expo services and be a member of accounts. */
|
||||
export type Actor = {
|
||||
id: Scalars['ID'];
|
||||
firstName?: Maybe<Scalars['String']>;
|
||||
created?: Maybe<Scalars['DateTime']>;
|
||||
isExpoAdmin?: Maybe<Scalars['Boolean']>;
|
||||
/** Associated accounts */
|
||||
accounts: Array<Account>;
|
||||
/** Access Tokens belonging to this actor */
|
||||
accessTokens: Array<AccessToken>;
|
||||
/**
|
||||
* Server feature gate values for this actor, optionally filtering by desired gates.
|
||||
* Only resolves for the viewer.
|
||||
*/
|
||||
featureGates: Scalars['JSONObject'];
|
||||
};
|
||||
|
||||
|
||||
/** A user or robot that can authenticate with Expo services and be a member of accounts. */
|
||||
export type ActorFeatureGatesArgs = {
|
||||
filter?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
/** A method of authentication for an Actor */
|
||||
export type AccessToken = {
|
||||
__typename?: 'AccessToken';
|
||||
id: Scalars['ID'];
|
||||
visibleTokenPrefix: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
revokedAt?: Maybe<Scalars['DateTime']>;
|
||||
lastUsedAt?: Maybe<Scalars['DateTime']>;
|
||||
owner: Actor;
|
||||
note?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
/** A second factor device belonging to a User */
|
||||
export type UserSecondFactorDevice = {
|
||||
__typename?: 'UserSecondFactorDevice';
|
||||
@@ -648,6 +647,19 @@ export enum SecondFactorMethod {
|
||||
Sms = 'SMS'
|
||||
}
|
||||
|
||||
/** A method of authentication for an Actor */
|
||||
export type AccessToken = {
|
||||
__typename?: 'AccessToken';
|
||||
id: Scalars['ID'];
|
||||
visibleTokenPrefix: Scalars['String'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
revokedAt?: Maybe<Scalars['DateTime']>;
|
||||
lastUsedAt?: Maybe<Scalars['DateTime']>;
|
||||
owner: Actor;
|
||||
note?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
/** An pending invitation sent to an email granting membership on an Account. */
|
||||
export type UserInvitation = {
|
||||
__typename?: 'UserInvitation';
|
||||
@@ -679,6 +691,7 @@ export enum Role {
|
||||
NotAdmin = 'NOT_ADMIN'
|
||||
}
|
||||
|
||||
|
||||
export enum AppPlatform {
|
||||
Ios = 'IOS',
|
||||
Android = 'ANDROID'
|
||||
@@ -727,12 +740,9 @@ export type Build = ActivityTimelineProjectActivity & {
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
metrics?: Maybe<BuildMetrics>;
|
||||
distribution?: Maybe<DistributionType>;
|
||||
};
|
||||
|
||||
export type ActivityTimelineProjectActivity = {
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
buildProfile?: Maybe<Scalars['String']>;
|
||||
gitCommitHash?: Maybe<Scalars['String']>;
|
||||
error?: Maybe<BuildError>;
|
||||
};
|
||||
|
||||
export type BuildArtifacts = {
|
||||
@@ -753,6 +763,12 @@ export enum DistributionType {
|
||||
Simulator = 'SIMULATOR'
|
||||
}
|
||||
|
||||
export type BuildError = {
|
||||
__typename?: 'BuildError';
|
||||
errorCode?: Maybe<Scalars['String']>;
|
||||
message?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
/** Represents an Standalone App build job */
|
||||
export type BuildJob = ActivityTimelineProjectActivity & {
|
||||
__typename?: 'BuildJob';
|
||||
@@ -1060,22 +1076,6 @@ export type UpdateBranchUpdatesArgs = {
|
||||
limit: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type Update = ActivityTimelineProjectActivity & {
|
||||
__typename?: 'Update';
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
branchId: Scalars['ID'];
|
||||
platform: Scalars['String'];
|
||||
manifestFragment: Scalars['String'];
|
||||
runtimeVersion: Scalars['String'];
|
||||
group: Scalars['String'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
message?: Maybe<Scalars['String']>;
|
||||
branch: UpdateBranch;
|
||||
};
|
||||
|
||||
export type EnvironmentSecret = {
|
||||
__typename?: 'EnvironmentSecret';
|
||||
id: Scalars['ID'];
|
||||
@@ -1149,6 +1149,24 @@ export type Charge = {
|
||||
receiptUrl?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type AccountQuery = {
|
||||
__typename?: 'AccountQuery';
|
||||
/** Query an Account by ID */
|
||||
byId: Account;
|
||||
/** Query an Account by name */
|
||||
byName: Account;
|
||||
};
|
||||
|
||||
|
||||
export type AccountQueryByIdArgs = {
|
||||
accountId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type AccountQueryByNameArgs = {
|
||||
accountName: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ActorQuery = {
|
||||
__typename?: 'ActorQuery';
|
||||
/** Query an Actor by ID */
|
||||
@@ -1573,19 +1591,9 @@ export type DeleteAccessTokenResult = {
|
||||
|
||||
export type AccountMutation = {
|
||||
__typename?: 'AccountMutation';
|
||||
/**
|
||||
* Add user to an account
|
||||
* @deprecated Use 'grantActorPermissions' mutation.
|
||||
*/
|
||||
grantAccess?: Maybe<Account>;
|
||||
/**
|
||||
* Revoke permissions for a given user
|
||||
* @deprecated Use 'revokeActorPermissions' mutation.
|
||||
*/
|
||||
revokeAccess?: Maybe<Account>;
|
||||
/** Add specified account Permissions for Actor */
|
||||
/** Add specified account Permissions for Actor. Actor must already have at least one permission on the account. */
|
||||
grantActorPermissions?: Maybe<Account>;
|
||||
/** Revoke specified Permissions for Actor */
|
||||
/** Revoke specified Permissions for Actor. Actor must already have at least one permission on the account. */
|
||||
revokeActorPermissions?: Maybe<Account>;
|
||||
/** Add a subscription */
|
||||
subscribeToProduct?: Maybe<Account>;
|
||||
@@ -1603,8 +1611,6 @@ export type AccountMutation = {
|
||||
setBuildAutoRenew?: Maybe<Account>;
|
||||
/** Set payment details */
|
||||
setPaymentSource?: Maybe<Account>;
|
||||
/** Extend offer to account */
|
||||
extendOffer?: Maybe<Account>;
|
||||
/** Send an email to primary account email */
|
||||
sendEmail?: Maybe<Account>;
|
||||
/** Require authorization to send push notifications for experiences owned by this account */
|
||||
@@ -1612,20 +1618,6 @@ export type AccountMutation = {
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationGrantAccessArgs = {
|
||||
accountName: Scalars['ID'];
|
||||
toUser: Scalars['ID'];
|
||||
accessLevels?: Maybe<Array<Maybe<Permission>>>;
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationRevokeAccessArgs = {
|
||||
accountName: Scalars['ID'];
|
||||
fromUser: Scalars['ID'];
|
||||
accessLevels?: Maybe<Array<Maybe<Permission>>>;
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationGrantActorPermissionsArgs = {
|
||||
accountID: Scalars['ID'];
|
||||
actorID: Scalars['ID'];
|
||||
@@ -1672,13 +1664,6 @@ export type AccountMutationSetPaymentSourceArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationExtendOfferArgs = {
|
||||
accountName: Scalars['ID'];
|
||||
offer: StandardOffer;
|
||||
suppressMessage?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
|
||||
export type AccountMutationSendEmailArgs = {
|
||||
accountName: Scalars['ID'];
|
||||
emailTemplate: EmailTemplate;
|
||||
@@ -1690,15 +1675,6 @@ export type AccountMutationSetPushSecurityEnabledArgs = {
|
||||
pushSecurityEnabled: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum StandardOffer {
|
||||
/** $29 USD per month, 30 day trial */
|
||||
Default = 'DEFAULT',
|
||||
/** $29 USD per month, 1 year trial */
|
||||
YcDeals = 'YC_DEALS',
|
||||
/** $800 USD per month */
|
||||
Support = 'SUPPORT'
|
||||
}
|
||||
|
||||
export enum EmailTemplate {
|
||||
/** Able to purchase Developer Services */
|
||||
DevServicesOfferExtended = 'DEV_SERVICES_OFFER_EXTENDED',
|
||||
@@ -2334,7 +2310,7 @@ export type PartialManifest = {
|
||||
};
|
||||
|
||||
export type PartialManifestAsset = {
|
||||
fileHash: Scalars['String'];
|
||||
fileSHA256: Scalars['String'];
|
||||
bundleKey: Scalars['String'];
|
||||
contentType: Scalars['String'];
|
||||
storageBucket: Scalars['String'];
|
||||
@@ -2352,6 +2328,11 @@ export type UserInvitationMutation = {
|
||||
resendUserInvitation: UserInvitation;
|
||||
/** Rescind UserInvitation by ID */
|
||||
deleteUserInvitation: RescindUserInvitationResult;
|
||||
/**
|
||||
* Delete UserInvitation by token. Note that the viewer's email is not required to match
|
||||
* the email on the invitation.
|
||||
*/
|
||||
deleteUserInvitationByToken: RescindUserInvitationResult;
|
||||
/** Accept UserInvitation by ID. Viewer must have matching email and email must be verified. */
|
||||
acceptUserInvitationAsViewer: AcceptUserInvitationResult;
|
||||
/**
|
||||
@@ -2380,6 +2361,11 @@ export type UserInvitationMutationDeleteUserInvitationArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type UserInvitationMutationDeleteUserInvitationByTokenArgs = {
|
||||
token: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type UserInvitationMutationAcceptUserInvitationAsViewerArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
@@ -2665,6 +2651,15 @@ export type DeleteEnvironmentSecretResult = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export enum StandardOffer {
|
||||
/** $29 USD per month, 30 day trial */
|
||||
Default = 'DEFAULT',
|
||||
/** $29 USD per month, 1 year trial */
|
||||
YcDeals = 'YC_DEALS',
|
||||
/** $800 USD per month */
|
||||
Support = 'SUPPORT'
|
||||
}
|
||||
|
||||
export type BaseSearchResult = SearchResult & {
|
||||
__typename?: 'BaseSearchResult';
|
||||
/** @deprecated Use SearchResult instead */
|
||||
@@ -2709,6 +2704,303 @@ export enum CacheControlScope {
|
||||
}
|
||||
|
||||
|
||||
export type CreateUpdateBranchForAppMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateUpdateBranchForAppMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateBranch: (
|
||||
{ __typename?: 'UpdateBranchMutation' }
|
||||
& { createUpdateBranchForApp?: Maybe<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetBranchInfoQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetBranchInfoQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateBranchByName: (
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
) }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type DeleteUpdateBranchMutationVariables = Exact<{
|
||||
branchId: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteUpdateBranchMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateBranch: (
|
||||
{ __typename?: 'UpdateBranchMutation' }
|
||||
& { deleteUpdateBranch: (
|
||||
{ __typename?: 'DeleteUpdateBranchResult' }
|
||||
& Pick<DeleteUpdateBranchResult, 'id'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type BranchesByAppQueryVariables = Exact<{
|
||||
fullName: Scalars['String'];
|
||||
limit: Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BranchesByAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id' | 'fullName'>
|
||||
& { updateBranches: Array<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'updatedAt' | 'message'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename: 'User' }
|
||||
& Pick<User, 'username' | 'id'>
|
||||
) | (
|
||||
{ __typename: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)> }
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
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;
|
||||
}>;
|
||||
|
||||
|
||||
export type EditUpdateBranchMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateBranch: (
|
||||
{ __typename?: 'UpdateBranchMutation' }
|
||||
& { editUpdateBranch: (
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type ViewBranchQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
limit: Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewBranchQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateBranchByName: (
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt' | 'runtimeVersion' | 'platform' | 'manifestFragment'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'firstName' | 'id'>
|
||||
) | (
|
||||
{ __typename?: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type CancelBuildMutationVariables = Exact<{
|
||||
buildId: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CancelBuildMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { build?: Maybe<(
|
||||
{ __typename?: 'BuildMutation' }
|
||||
& { cancel: (
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id' | 'status'>
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type CreateUpdateChannelOnAppMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
branchMapping: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateUpdateChannelOnAppMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateChannel: (
|
||||
{ __typename?: 'UpdateChannelMutation' }
|
||||
& { createUpdateChannelForApp?: Maybe<(
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name' | 'branchMapping'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetChannelByNameToEditQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
channelName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetChannelByNameToEditQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateChannelByName: (
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name'>
|
||||
& { updateBranches: Array<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
)> }
|
||||
) }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type UpdateChannelBranchMappingMutationVariables = Exact<{
|
||||
channelId: Scalars['ID'];
|
||||
branchMapping: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateChannelBranchMappingMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateChannel: (
|
||||
{ __typename?: 'UpdateChannelMutation' }
|
||||
& { editUpdateChannel?: Maybe<(
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name' | 'branchMapping'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetAllChannelsForAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAllChannelsForAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateChannels: Array<(
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name'>
|
||||
& { updateBranches: Array<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'firstName' | 'id'>
|
||||
) | (
|
||||
{ __typename?: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)> }
|
||||
)> }
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type GetChannelByNameForAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
channelName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetChannelByNameForAppQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byId: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { updateChannelByName: (
|
||||
{ __typename?: 'UpdateChannel' }
|
||||
& Pick<UpdateChannel, 'id' | 'name' | 'createdAt'>
|
||||
& { updateBranches: Array<(
|
||||
{ __typename?: 'UpdateBranch' }
|
||||
& Pick<UpdateBranch, 'id' | 'name'>
|
||||
& { updates: Array<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'id' | 'group' | 'message' | 'createdAt'>
|
||||
& { actor?: Maybe<(
|
||||
{ __typename?: 'User' }
|
||||
& Pick<User, 'firstName' | 'id'>
|
||||
) | (
|
||||
{ __typename?: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
)> }
|
||||
)> }
|
||||
)> }
|
||||
) }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type CreateAppleAppIdentifierMutationVariables = Exact<{
|
||||
appleAppIdentifierInput: AppleAppIdentifierInput;
|
||||
accountId: Scalars['ID'];
|
||||
@@ -3315,6 +3607,56 @@ export type CommonIosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery =
|
||||
)> }
|
||||
);
|
||||
|
||||
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
accountId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateEnvironmentSecretForAccountMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { environmentSecret: (
|
||||
{ __typename?: 'EnvironmentSecretMutation' }
|
||||
& { createEnvironmentSecretForAccount: (
|
||||
{ __typename?: 'EnvironmentSecret' }
|
||||
& EnvironmentSecretFragment
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CreateEnvironmentSecretForAppMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
appId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateEnvironmentSecretForAppMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { environmentSecret: (
|
||||
{ __typename?: 'EnvironmentSecretMutation' }
|
||||
& { createEnvironmentSecretForApp: (
|
||||
{ __typename?: 'EnvironmentSecret' }
|
||||
& EnvironmentSecretFragment
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type DeleteEnvironmentSecretMutationVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteEnvironmentSecretMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { environmentSecret: (
|
||||
{ __typename?: 'EnvironmentSecretMutation' }
|
||||
& { deleteEnvironmentSecret: (
|
||||
{ __typename?: 'DeleteEnvironmentSecretResult' }
|
||||
& Pick<DeleteEnvironmentSecretResult, 'id'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetSignedUploadMutationVariables = Exact<{
|
||||
contentTypes: Array<Scalars['String']>;
|
||||
}>;
|
||||
@@ -3415,6 +3757,46 @@ export type PendingBuildsForAccountAndPlatformQuery = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type EnvironmentSecretsByAccountNameQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type EnvironmentSecretsByAccountNameQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { account: (
|
||||
{ __typename?: 'AccountQuery' }
|
||||
& { byName: (
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id'>
|
||||
& { environmentSecrets: Array<(
|
||||
{ __typename?: 'EnvironmentSecret' }
|
||||
& EnvironmentSecretFragment
|
||||
)> }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type EnvironmentSecretsByAppFullNameQueryVariables = Exact<{
|
||||
fullName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type EnvironmentSecretsByAppFullNameQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { app?: Maybe<(
|
||||
{ __typename?: 'AppQuery' }
|
||||
& { byFullName: (
|
||||
{ __typename?: 'App' }
|
||||
& Pick<App, 'id'>
|
||||
& { environmentSecrets: Array<(
|
||||
{ __typename?: 'EnvironmentSecret' }
|
||||
& EnvironmentSecretFragment
|
||||
)> }
|
||||
) }
|
||||
)> }
|
||||
);
|
||||
|
||||
export type ProjectByUsernameAndSlugQueryVariables = Exact<{
|
||||
username: Scalars['String'];
|
||||
slug: Scalars['String'];
|
||||
@@ -3478,6 +3860,11 @@ export type AppFragment = (
|
||||
& Pick<App, 'id' | 'fullName' | 'slug'>
|
||||
);
|
||||
|
||||
export type EnvironmentSecretFragment = (
|
||||
{ __typename?: 'EnvironmentSecret' }
|
||||
& Pick<EnvironmentSecret, 'id' | 'name' | 'createdAt'>
|
||||
);
|
||||
|
||||
export type AppleAppIdentifierFragment = (
|
||||
{ __typename?: 'AppleAppIdentifier' }
|
||||
& Pick<AppleAppIdentifier, 'id' | 'bundleIdentifier'>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
CreateEnvironmentSecretForAccountMutation,
|
||||
CreateEnvironmentSecretForAppMutation,
|
||||
DeleteEnvironmentSecretMutation,
|
||||
EnvironmentSecretFragment,
|
||||
} from '../generated';
|
||||
import { EnvironmentSecretFragmentNode } from '../types/EnvironmentSecret';
|
||||
|
||||
export const EnvironmentSecretMutation = {
|
||||
async createForAccount(
|
||||
input: { name: string; value: string },
|
||||
accountId: string
|
||||
): Promise<EnvironmentSecretFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateEnvironmentSecretForAccountMutation>(
|
||||
gql`
|
||||
mutation CreateEnvironmentSecretForAccount(
|
||||
$input: CreateEnvironmentSecretInput!
|
||||
$accountId: String!
|
||||
) {
|
||||
environmentSecret {
|
||||
createEnvironmentSecretForAccount(
|
||||
environmentSecretData: $input
|
||||
accountId: $accountId
|
||||
) {
|
||||
id
|
||||
...EnvironmentSecretFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(EnvironmentSecretFragmentNode)}
|
||||
`,
|
||||
{ input, accountId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.environmentSecret.createEnvironmentSecretForAccount;
|
||||
},
|
||||
async createForApp(
|
||||
input: { name: string; value: string },
|
||||
appId: string
|
||||
): Promise<EnvironmentSecretFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<CreateEnvironmentSecretForAppMutation>(
|
||||
gql`
|
||||
mutation CreateEnvironmentSecretForApp(
|
||||
$input: CreateEnvironmentSecretInput!
|
||||
$appId: String!
|
||||
) {
|
||||
environmentSecret {
|
||||
createEnvironmentSecretForApp(environmentSecretData: $input, appId: $appId) {
|
||||
id
|
||||
...EnvironmentSecretFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(EnvironmentSecretFragmentNode)}
|
||||
`,
|
||||
{ input, appId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.environmentSecret.createEnvironmentSecretForApp;
|
||||
},
|
||||
async delete(id: string): Promise<{ id: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<DeleteEnvironmentSecretMutation>(
|
||||
gql`
|
||||
mutation DeleteEnvironmentSecret($id: String!) {
|
||||
environmentSecret {
|
||||
deleteEnvironmentSecret(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.environmentSecret.deleteEnvironmentSecret;
|
||||
},
|
||||
};
|
||||
@@ -31,7 +31,7 @@ const PublishMutation = {
|
||||
|
||||
async publishUpdateGroupAsync(
|
||||
publishUpdateGroupInput: PublishUpdateGroupInput
|
||||
): Promise<Pick<Update, 'id' | 'group'>> {
|
||||
): Promise<Pick<Update, 'group'>> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<UpdatePublishMutation>(
|
||||
@@ -49,7 +49,8 @@ const PublishMutation = {
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateBranch.publishUpdateGroup[0]!;
|
||||
const { group } = data.updateBranch.publishUpdateGroup[0]!;
|
||||
return { group };
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
EnvironmentSecretFragment,
|
||||
EnvironmentSecretsByAccountNameQuery,
|
||||
EnvironmentSecretsByAppFullNameQuery,
|
||||
} from '../generated';
|
||||
import { EnvironmentSecretFragmentNode } from '../types/EnvironmentSecret';
|
||||
|
||||
export const EnvironmentSecretsQuery = {
|
||||
async byAcccountNameAsync(accountName: string): Promise<EnvironmentSecretFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<EnvironmentSecretsByAccountNameQuery>(
|
||||
gql`
|
||||
query EnvironmentSecretsByAccountName($accountName: String!) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
id
|
||||
environmentSecrets {
|
||||
id
|
||||
...EnvironmentSecretFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(EnvironmentSecretFragmentNode)}
|
||||
`,
|
||||
{ accountName }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byName.environmentSecrets;
|
||||
},
|
||||
async byAppFullNameAsync(fullName: string): Promise<EnvironmentSecretFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<EnvironmentSecretsByAppFullNameQuery>(
|
||||
gql`
|
||||
query EnvironmentSecretsByAppFullName($fullName: String!) {
|
||||
app {
|
||||
byFullName(fullName: $fullName) {
|
||||
id
|
||||
environmentSecrets {
|
||||
id
|
||||
...EnvironmentSecretFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(EnvironmentSecretFragmentNode)}
|
||||
`,
|
||||
{ fullName }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.app?.byFullName.environmentSecrets ?? [];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const EnvironmentSecretFragmentNode = gql`
|
||||
fragment EnvironmentSecretFragment on EnvironmentSecret {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
}
|
||||
`;
|
||||
@@ -78,15 +78,20 @@ export default class Log {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format links as dim with an underline.
|
||||
* Format links as dim (unless disabled) with an underline.
|
||||
*
|
||||
* @example Learn more: https://expo.io
|
||||
* @param url
|
||||
*/
|
||||
export function learnMore(url: string, learnMoreMessage?: string): string {
|
||||
export function learnMore(
|
||||
url: string,
|
||||
{ learnMoreMessage, dim = true }: { learnMoreMessage?: string; dim?: boolean } = {}
|
||||
): string {
|
||||
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
|
||||
if (terminalLink.isSupported) {
|
||||
return chalk.dim(terminalLink(learnMoreMessage ?? 'Learn more.', url));
|
||||
const text = terminalLink(learnMoreMessage ?? 'Learn more.', url);
|
||||
return dim ? chalk.dim(text) : text;
|
||||
}
|
||||
return chalk.dim(`${learnMoreMessage ?? 'Learn more'}: ${chalk.underline(url)}`);
|
||||
const text = `${learnMoreMessage ?? 'Learn more'}: ${chalk.underline(url)}`;
|
||||
return dim ? chalk.dim(text) : text;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
@@ -64,6 +63,7 @@ describe(getProjectAccountName, () => {
|
||||
__typename: 'User',
|
||||
id: 'userId',
|
||||
username: 'notbrent',
|
||||
accounts: [],
|
||||
});
|
||||
expect(projectAccountName).toBe(expWithOwner.owner);
|
||||
});
|
||||
@@ -73,6 +73,7 @@ describe(getProjectAccountName, () => {
|
||||
__typename: 'Robot',
|
||||
id: 'userId',
|
||||
firstName: 'notauser',
|
||||
accounts: [],
|
||||
});
|
||||
expect(projectAccountName).toBe(expWithOwner.owner);
|
||||
});
|
||||
@@ -82,6 +83,7 @@ describe(getProjectAccountName, () => {
|
||||
__typename: 'User',
|
||||
id: 'userId',
|
||||
username: 'dominik',
|
||||
accounts: [],
|
||||
});
|
||||
expect(projectAccountName).toBe('dominik');
|
||||
});
|
||||
@@ -92,23 +94,21 @@ describe(getProjectAccountName, () => {
|
||||
__typename: 'Robot',
|
||||
id: 'userId',
|
||||
firstName: 'notauser',
|
||||
accounts: [],
|
||||
});
|
||||
expect(resolveProjectAccountName).toThrow('manifest property is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe(getProjectAccountNameAsync, () => {
|
||||
const expWithOwner: any = { owner: 'dominik' };
|
||||
const expWithoutOwner: any = {};
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(getConfig).mockReset();
|
||||
asMock(getUserAsync).mockReset();
|
||||
});
|
||||
|
||||
it(`returns the owner field's value from app.json / app.config.js`, async () => {
|
||||
asMock(getConfig).mockImplementation(() => ({
|
||||
exp: {
|
||||
owner: 'dominik',
|
||||
},
|
||||
}));
|
||||
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
@@ -119,14 +119,11 @@ describe(getProjectAccountNameAsync, () => {
|
||||
],
|
||||
}));
|
||||
|
||||
const projectAccountName = await getProjectAccountNameAsync('/app');
|
||||
const projectAccountName = await getProjectAccountNameAsync(expWithOwner);
|
||||
expect(projectAccountName).toBe('dominik');
|
||||
});
|
||||
|
||||
it(`returns the username if owner field is not set in app.json / app.config.js`, async () => {
|
||||
asMock(getConfig).mockImplementation(() => ({
|
||||
exp: {},
|
||||
}));
|
||||
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
|
||||
__typename: 'User',
|
||||
id: 'user_id',
|
||||
@@ -137,25 +134,19 @@ describe(getProjectAccountNameAsync, () => {
|
||||
],
|
||||
}));
|
||||
|
||||
const projectAccountName = await getProjectAccountNameAsync('/app');
|
||||
const projectAccountName = await getProjectAccountNameAsync(expWithoutOwner);
|
||||
expect(projectAccountName).toBe('notnotbrent');
|
||||
});
|
||||
|
||||
it(`throws an error if the user is not logged in`, async () => {
|
||||
asMock(getConfig).mockImplementation(() => ({
|
||||
exp: {
|
||||
owner: 'dominik',
|
||||
},
|
||||
}));
|
||||
asMock(getUserAsync).mockImplementation((): Actor | undefined => undefined);
|
||||
|
||||
await expect(getProjectAccountNameAsync('/app')).rejects.toThrow(/Failed to access user data/);
|
||||
await expect(getProjectAccountNameAsync(expWithOwner)).rejects.toThrow(
|
||||
/Failed to access user data/
|
||||
);
|
||||
});
|
||||
|
||||
it(`throws when project owner is undefined for robot actors`, async () => {
|
||||
asMock(getConfig).mockImplementation(() => ({
|
||||
exp: {},
|
||||
}));
|
||||
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
|
||||
__typename: 'Robot',
|
||||
id: 'user_id',
|
||||
@@ -165,7 +156,7 @@ describe(getProjectAccountNameAsync, () => {
|
||||
{ id: 'account_id_2', name: 'dominik' },
|
||||
],
|
||||
}));
|
||||
await expect(getProjectAccountNameAsync('/app')).rejects.toThrow(
|
||||
await expect(getProjectAccountNameAsync(expWithoutOwner)).rejects.toThrow(
|
||||
'manifest property is required'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -140,7 +140,7 @@ describe(convertAssetToUpdateInfoGroupFormatAsync, () => {
|
||||
await expect(convertAssetToUpdateInfoGroupFormatAsync(asset)).resolves.toEqual({
|
||||
bundleKey: `c939e759656f577c058f445bfb19182e.${type}`,
|
||||
contentType: 'image/jpeg',
|
||||
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
});
|
||||
@@ -177,7 +177,7 @@ describe(buildUpdateInfoGroupAsync, () => {
|
||||
{
|
||||
bundleKey: 'c939e759656f577c058f445bfb19182e.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
fileSHA256: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
},
|
||||
@@ -186,7 +186,7 @@ describe(buildUpdateInfoGroupAsync, () => {
|
||||
launchAsset: {
|
||||
bundleKey: 'ec0dd14670aae108f99a810df9c1482c.bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
fileHash: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
|
||||
fileSHA256: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'aC9N6RZlcHoIYjIsoJd2KUcigBKy98RHvZacDyPNjCQ',
|
||||
},
|
||||
@@ -206,7 +206,9 @@ describe(resolveInputDirectory, () => {
|
||||
expect(() => {
|
||||
resolveInputDirectory(nonExistentPath);
|
||||
}).toThrow(`The input directory "${nonExistentPath}" does not exist.
|
||||
You'll need to run a command to build the JS bundle first (e.g. 'expo export').
|
||||
You can allow us to build it for you by not setting the --skip-bundler flag.
|
||||
If you chose to build it yourself you'll need to run a command to build the JS
|
||||
bundle first.
|
||||
You can use '--input-dir' to specify a different input directory.`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpoConfig, getConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { AndroidConfig, IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
@@ -26,8 +26,7 @@ export function getProjectAccountName(exp: ExpoConfig, user: Actor): string {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjectAccountNameAsync(projectDir: string): Promise<string> {
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
export async function getProjectAccountNameAsync(exp: ExpoConfig): Promise<string> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
return getProjectAccountName(exp, user);
|
||||
}
|
||||
@@ -37,8 +36,7 @@ export async function findProjectRootAsync(cwd?: string): Promise<string | null>
|
||||
return projectRootDir ?? null;
|
||||
}
|
||||
|
||||
export async function getProjectIdAsync(projectDir: string): Promise<string> {
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
export async function getProjectIdAsync(exp: ExpoConfig): Promise<string> {
|
||||
return await ensureProjectExistsAsync({
|
||||
accountName: getProjectAccountName(exp, await ensureLoggedInAsync()),
|
||||
projectName: exp.slug,
|
||||
@@ -46,10 +44,8 @@ export async function getProjectIdAsync(projectDir: string): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProjectFullNameAsync(projectDir: string): Promise<string> {
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
export async function getProjectFullNameAsync(exp: ExpoConfig): Promise<string> {
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
return `@${accountName}/${exp.slug}`;
|
||||
}
|
||||
|
||||
@@ -65,13 +61,17 @@ export async function getAndroidApplicationIdAsync(projectDir: string): Promise<
|
||||
return matchResult?.[1] ?? null;
|
||||
}
|
||||
|
||||
export async function getAppIdentifierAsync(
|
||||
projectDir: string,
|
||||
platform: Platform
|
||||
): Promise<string | null> {
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
export async function getAppIdentifierAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string | null> {
|
||||
switch (platform) {
|
||||
case Platform.Android: {
|
||||
case Platform.ANDROID: {
|
||||
const packageNameFromConfig = AndroidConfig.Package.getPackage(exp);
|
||||
if (packageNameFromConfig) {
|
||||
return packageNameFromConfig;
|
||||
@@ -80,7 +80,7 @@ export async function getAppIdentifierAsync(
|
||||
? await getAndroidApplicationIdAsync(projectDir)
|
||||
: null;
|
||||
}
|
||||
case Platform.iOS: {
|
||||
case Platform.IOS: {
|
||||
return (
|
||||
IOSConfig.BundleIdentifier.getBundleIdentifier(exp) ??
|
||||
IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(projectDir)
|
||||
@@ -89,14 +89,19 @@ export async function getAppIdentifierAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureAppIdentifierIsDefinedAsync(
|
||||
projectDir: string,
|
||||
platform: Platform
|
||||
): Promise<string> {
|
||||
const appIdentifier = await getAppIdentifierAsync(projectDir, platform);
|
||||
export async function ensureAppIdentifierIsDefinedAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
const appIdentifier = await getAppIdentifierAsync({ projectDir, platform, exp });
|
||||
if (!appIdentifier) {
|
||||
const desc = getProjectConfigDescription(projectDir);
|
||||
const fieldStr = platform === Platform.Android ? 'android.package' : 'ios.bundleIdentifier';
|
||||
const fieldStr = platform === Platform.ANDROID ? 'android.package' : 'ios.bundleIdentifier';
|
||||
throw new Error(`Please define "${fieldStr}" in your ${desc}.`);
|
||||
}
|
||||
return appIdentifier;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Platform } from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import Joi from '@hapi/joi';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
@@ -10,6 +11,7 @@ import path from 'path';
|
||||
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
|
||||
import { PublishMutation } from '../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../graphql/queries/PublishQuery';
|
||||
import Log from '../log';
|
||||
import { PresignedPost, uploadWithPresignedPostAsync } from '../uploads';
|
||||
|
||||
export const TIMEOUT_LIMIT = 60_000; // 1 minute
|
||||
@@ -102,23 +104,23 @@ async function calculateFileHashAsync(filePath: string, algorithm: string): Prom
|
||||
* Convenience function that computes an assets storage key starting from its buffer.
|
||||
*/
|
||||
export async function getStorageKeyForAssetAsync(asset: RawAsset): Promise<string> {
|
||||
const fileHash = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
|
||||
return getStorageKey(asset.contentType, fileHash);
|
||||
const fileSHA256 = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
|
||||
return getStorageKey(asset.contentType, fileSHA256);
|
||||
}
|
||||
|
||||
export async function convertAssetToUpdateInfoGroupFormatAsync(
|
||||
asset: RawAsset
|
||||
): Promise<PartialManifestAsset> {
|
||||
const fileHash = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
|
||||
const fileSHA256 = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
|
||||
const contentType = asset.contentType;
|
||||
const storageKey = getStorageKey(contentType, fileHash);
|
||||
const storageKey = getStorageKey(contentType, fileSHA256);
|
||||
const bundleKey = [
|
||||
(await calculateFileHashAsync(asset.path, 'md5')).toString('hex'),
|
||||
asset.type,
|
||||
].join('.');
|
||||
|
||||
return {
|
||||
fileHash,
|
||||
fileSHA256,
|
||||
contentType,
|
||||
storageBucket: STORAGE_BUCKET,
|
||||
storageKey,
|
||||
@@ -140,11 +142,50 @@ export async function buildUpdateInfoGroupAsync(assets: CollectedAssets): Promis
|
||||
return updateInfoGroup as UpdateInfoGroup;
|
||||
}
|
||||
|
||||
export async function buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
}: {
|
||||
projectDir: string;
|
||||
inputDir: string;
|
||||
}) {
|
||||
const packageJSON = JsonFile.read(path.resolve(projectDir, 'package.json'));
|
||||
if (!packageJSON) {
|
||||
throw new Error('Could not locate package.json');
|
||||
}
|
||||
|
||||
Log.withTick(`Building bundle with expo-cli...`);
|
||||
const spawnPromise = spawnAsync(
|
||||
'yarn',
|
||||
['expo', 'export', '--output-dir', inputDir, '--experimental-bundle', '--force'],
|
||||
{ stdio: ['inherit', 'pipe', 'pipe'] } // inherit stdin so user can install a missing expo-cli from inside this command
|
||||
);
|
||||
const {
|
||||
child: { stdout, stderr },
|
||||
} = spawnPromise;
|
||||
if (!(stdout && stderr)) {
|
||||
throw new Error('Failed to spawn expo-cli');
|
||||
}
|
||||
stdout.on('data', data => {
|
||||
for (const line of data.toString().trim().split('\n')) {
|
||||
Log.log(`[expo-cli]${line}`);
|
||||
}
|
||||
});
|
||||
stderr.on('data', data => {
|
||||
for (const line of data.toString().trim().split('\n')) {
|
||||
Log.warn(`[expo-cli]${line}`);
|
||||
}
|
||||
});
|
||||
await spawnPromise;
|
||||
}
|
||||
|
||||
export function resolveInputDirectory(customInputDirectory: string): string {
|
||||
const distRoot = path.resolve(customInputDirectory);
|
||||
if (!fs.existsSync(distRoot)) {
|
||||
throw new Error(`The input directory "${customInputDirectory}" does not exist.
|
||||
You'll need to run a command to build the JS bundle first (e.g. 'expo export').
|
||||
You can allow us to build it for you by not setting the --skip-bundler flag.
|
||||
If you chose to build it yourself you'll need to run a command to build the JS
|
||||
bundle first.
|
||||
You can use '--input-dir' to specify a different input directory.`);
|
||||
}
|
||||
return distRoot;
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function pressAnyKeyToContinueAsync() {
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
await new Promise(res => {
|
||||
await new Promise<void>(res => {
|
||||
process.stdin.on('data', key => {
|
||||
if (String(key) === '\u0003') {
|
||||
process.exit(constants.signals.SIGINT + 128); // ctrl-c
|
||||
|
||||
@@ -21,7 +21,7 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
|
||||
protected async startSubmissionAsync(
|
||||
submissionConfig: SubmissionConfig,
|
||||
verbose: boolean = false
|
||||
) {
|
||||
): Promise<SubmissionStatus> {
|
||||
Log.addNewLineIfNone();
|
||||
const scheduleSpinner = ora('Scheduling submission').start();
|
||||
let submissionId: string;
|
||||
@@ -65,6 +65,7 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
|
||||
}
|
||||
|
||||
await displayLogs(submission, submissionStatus, verbose);
|
||||
return submissionStatus ?? SubmissionStatus.ERRORED;
|
||||
}
|
||||
|
||||
private getStatusText(status: SubmissionStatus): string {
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
ArchiveType,
|
||||
SubmissionPlatform,
|
||||
} from '../types';
|
||||
import { printSummary } from '../utils/summary';
|
||||
import {
|
||||
ArchiveSourceSummaryFields,
|
||||
formatArchiveSourceSummary,
|
||||
printSummary,
|
||||
} from '../utils/summary';
|
||||
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
|
||||
import { AndroidSubmissionConfig, ReleaseStatus, ReleaseTrack } from './AndroidSubmissionConfig';
|
||||
import { ServiceAccountSource, getServiceAccountAsync } from './ServiceAccountSource';
|
||||
@@ -35,10 +39,14 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
|
||||
async submitAsync(): Promise<void> {
|
||||
const resolvedSourceOptions = await this.resolveSourceOptions();
|
||||
const submissionConfig = await this.formatSubmissionConfigAndPrintSummary(
|
||||
this.options,
|
||||
resolvedSourceOptions
|
||||
const submissionConfig = await this.formatSubmissionConfig(this.options, resolvedSourceOptions);
|
||||
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
|
||||
await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
|
||||
}
|
||||
|
||||
@@ -53,71 +61,66 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
};
|
||||
}
|
||||
|
||||
private async formatSubmissionConfigAndPrintSummary(
|
||||
private async formatSubmissionConfig(
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
|
||||
): Promise<AndroidSubmissionConfig> {
|
||||
const serviceAccount = await fs.readFile(serviceAccountPath, 'utf-8');
|
||||
const { track, releaseStatus, projectId } = options;
|
||||
const submissionConfig = {
|
||||
|
||||
// structuring order affects table rows order
|
||||
return {
|
||||
androidPackage,
|
||||
archiveUrl: archive.location,
|
||||
archiveType: archive.type as AndroidArchiveType,
|
||||
track,
|
||||
releaseStatus,
|
||||
projectId,
|
||||
serviceAccount,
|
||||
};
|
||||
}
|
||||
|
||||
printSummaryAndroid({
|
||||
...submissionConfig,
|
||||
private prepareSummaryData(
|
||||
options: AndroidSubmissionOptions,
|
||||
{ archive, androidPackage, serviceAccountPath }: ResolvedSourceOptions
|
||||
): Summary {
|
||||
const { projectId, track, releaseStatus } = options;
|
||||
|
||||
return {
|
||||
projectId,
|
||||
androidPackage,
|
||||
track,
|
||||
releaseStatus,
|
||||
archiveType: archive.type as AndroidArchiveType,
|
||||
...formatArchiveSourceSummary(archive),
|
||||
serviceAccountPath,
|
||||
});
|
||||
return { ...submissionConfig, serviceAccount };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the summary as a table. Exported for testing locally.
|
||||
*
|
||||
* @example
|
||||
* printSummaryAndroid({
|
||||
* androidPackage: 'com.expo.demoapp',
|
||||
* archivePath: '/Users/example/Documents/863f9337-65d2-40c6-acb3-c1054c5c09f8.apk',
|
||||
* archiveUrl: 'https://turtle-v2-artifacts.s3.amazonaws.com/ios/6420592d-5b5d-439b-aed4-ccd278647138-ca4145d8468947df9ded737248a1a238.aab',
|
||||
* archiveType: 'apk' as any,
|
||||
* serviceAccountPath: '/Users/example/Documents/gsa.json',
|
||||
* track: ReleaseTrack.production,
|
||||
* releaseStatus: ReleaseStatus.completed,
|
||||
* projectId: '863f9337-65d2-40c6-acb3-c1054c5c09f8',
|
||||
* });
|
||||
* @param submissionConfig
|
||||
*/
|
||||
export function printSummaryAndroid(submissionConfig: Summary) {
|
||||
printSummary(submissionConfig, SummaryHumanReadableKeys, SummaryHumanReadableValues);
|
||||
}
|
||||
|
||||
interface Summary {
|
||||
type Summary = {
|
||||
androidPackage: string;
|
||||
archivePath?: string;
|
||||
archiveUrl?: string;
|
||||
archiveType: ArchiveType;
|
||||
serviceAccountPath: string;
|
||||
track: ReleaseTrack;
|
||||
releaseStatus?: ReleaseStatus;
|
||||
projectId?: string;
|
||||
}
|
||||
} & ArchiveSourceSummaryFields;
|
||||
|
||||
const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
|
||||
androidPackage: 'Android package',
|
||||
archivePath: 'Archive path',
|
||||
archiveUrl: 'Download URL',
|
||||
archiveType: 'Archive type',
|
||||
serviceAccountPath: 'Google Service Account',
|
||||
buildId: 'Build ID',
|
||||
serviceAccountPath: 'Google Service Key',
|
||||
track: 'Release track',
|
||||
releaseStatus: 'Release status',
|
||||
projectId: 'Project ID',
|
||||
};
|
||||
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {};
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {
|
||||
archiveType: (type: string) => type.toUpperCase(),
|
||||
};
|
||||
|
||||
export default AndroidSubmitter;
|
||||
@@ -53,6 +53,11 @@ interface ArchiveFilePromptSource extends ArchiveFileSourceBase {
|
||||
projectDir: string;
|
||||
}
|
||||
|
||||
interface ResolvedArchive {
|
||||
location: string;
|
||||
realSource: ArchiveFileSource;
|
||||
}
|
||||
|
||||
export type ArchiveFileSource =
|
||||
| ArchiveFileUrlSource
|
||||
| ArchiveFileLatestSource
|
||||
@@ -60,25 +65,31 @@ export type ArchiveFileSource =
|
||||
| ArchiveFileBuildIdSource
|
||||
| ArchiveFilePromptSource;
|
||||
|
||||
export async function getArchiveFileLocationAsync(source: ArchiveFileSource): Promise<string> {
|
||||
export async function getArchiveFileLocationAsync(
|
||||
source: ArchiveFileSource
|
||||
): Promise<ResolvedArchive> {
|
||||
switch (source.sourceType) {
|
||||
case ArchiveFileSourceType.prompt:
|
||||
return await handlePromptSourceAsync(source);
|
||||
case ArchiveFileSourceType.url: {
|
||||
const url = await handleUrlSourceAsync(source);
|
||||
return await getArchiveLocationForUrlAsync(url);
|
||||
const urlArchive = await handleUrlSourceAsync(source);
|
||||
const resolvedUrl = await getArchiveLocationForUrlAsync(urlArchive.location);
|
||||
return { ...urlArchive, location: resolvedUrl };
|
||||
}
|
||||
case ArchiveFileSourceType.latest: {
|
||||
const url = await handleLatestSourceAsync(source);
|
||||
return await getArchiveLocationForUrlAsync(url);
|
||||
const urlArchive = await handleLatestSourceAsync(source);
|
||||
const resolvedUrl = await getArchiveLocationForUrlAsync(urlArchive.location);
|
||||
return { ...urlArchive, location: resolvedUrl };
|
||||
}
|
||||
case ArchiveFileSourceType.path: {
|
||||
const path = await handlePathSourceAsync(source);
|
||||
return getArchiveLocationForPathAsync(path);
|
||||
const pathArchive = await handlePathSourceAsync(source);
|
||||
const resolvedUrl = await getArchiveLocationForPathAsync(pathArchive.location);
|
||||
return { ...pathArchive, location: resolvedUrl };
|
||||
}
|
||||
case ArchiveFileSourceType.buildId: {
|
||||
const url = await handleBuildIdSourceAsync(source);
|
||||
return await getArchiveLocationForUrlAsync(url);
|
||||
const urlArchive = await handleBuildIdSourceAsync(source);
|
||||
const resolvedUrl = await getArchiveLocationForUrlAsync(urlArchive.location);
|
||||
return { ...urlArchive, location: resolvedUrl };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,11 +113,14 @@ async function getArchiveLocationForPathAsync(path: string): Promise<string> {
|
||||
return await uploadAppArchiveAsync(resolvedPath);
|
||||
}
|
||||
|
||||
async function handleUrlSourceAsync(source: ArchiveFileUrlSource): Promise<string> {
|
||||
return source.url;
|
||||
async function handleUrlSourceAsync(source: ArchiveFileUrlSource): Promise<ResolvedArchive> {
|
||||
return {
|
||||
location: source.url,
|
||||
realSource: source,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise<string> {
|
||||
async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise<ResolvedArchive> {
|
||||
try {
|
||||
const artifactUrl = await getLatestBuildArtifactUrlAsync(source.platform, source.projectId);
|
||||
|
||||
@@ -122,14 +136,17 @@ async function handleLatestSourceAsync(source: ArchiveFileLatestSource): Promise
|
||||
});
|
||||
}
|
||||
|
||||
return artifactUrl;
|
||||
return {
|
||||
location: artifactUrl,
|
||||
realSource: source,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePathSourceAsync(source: ArchiveFilePathSource): Promise<string> {
|
||||
async function handlePathSourceAsync(source: ArchiveFilePathSource): Promise<ResolvedArchive> {
|
||||
if (!(await isExistingFile(source.path))) {
|
||||
Log.error(chalk.bold(`${source.path} doesn't exist`));
|
||||
return getArchiveFileLocationAsync({
|
||||
@@ -137,12 +154,20 @@ async function handlePathSourceAsync(source: ArchiveFilePathSource): Promise<str
|
||||
sourceType: ArchiveFileSourceType.prompt,
|
||||
});
|
||||
}
|
||||
return source.path;
|
||||
return {
|
||||
location: source.path,
|
||||
realSource: source,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleBuildIdSourceAsync(source: ArchiveFileBuildIdSource): Promise<string> {
|
||||
async function handleBuildIdSourceAsync(
|
||||
source: ArchiveFileBuildIdSource
|
||||
): Promise<ResolvedArchive> {
|
||||
try {
|
||||
return await getBuildArtifactUrlByIdAsync(source.platform, source.id);
|
||||
return {
|
||||
location: await getBuildArtifactUrlByIdAsync(source.platform, source.id),
|
||||
realSource: source,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(chalk.bold(`Couldn't find build for id ${source.id}`));
|
||||
Log.error(err);
|
||||
@@ -153,7 +178,7 @@ async function handleBuildIdSourceAsync(source: ArchiveFileBuildIdSource): Promi
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise<string> {
|
||||
async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise<ResolvedArchive> {
|
||||
const { sourceType: sourceTypeRaw } = await promptAsync({
|
||||
name: 'sourceType',
|
||||
type: 'select',
|
||||
@@ -185,7 +210,7 @@ async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise
|
||||
});
|
||||
}
|
||||
case ArchiveFileSourceType.path: {
|
||||
const path = await askForArchivePathAsync();
|
||||
const path = await askForArchivePathAsync(source.platform);
|
||||
return getArchiveFileLocationAsync({
|
||||
...source,
|
||||
sourceType: ArchiveFileSourceType.path,
|
||||
@@ -211,6 +236,8 @@ async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise
|
||||
}
|
||||
}
|
||||
|
||||
/* PROMPTS */
|
||||
|
||||
async function askForArchiveUrlAsync(): Promise<string> {
|
||||
const defaultArchiveUrl = 'https://url.to/your/archive.aab';
|
||||
const { url } = await promptAsync({
|
||||
@@ -231,11 +258,12 @@ async function askForArchiveUrlAsync(): Promise<string> {
|
||||
return url;
|
||||
}
|
||||
|
||||
async function askForArchivePathAsync(): Promise<string> {
|
||||
const defaultArchivePath = '/path/to/your/archive.aab';
|
||||
async function askForArchivePathAsync(platform: SubmissionPlatform): Promise<string> {
|
||||
const isIos = platform === SubmissionPlatform.iOS;
|
||||
const defaultArchivePath = `/path/to/your/archive.${isIos ? 'ipa' : 'aab'}`;
|
||||
const { path } = await promptAsync({
|
||||
name: 'path',
|
||||
message: 'Path to the app archive file (aab or apk):',
|
||||
message: `Path to the app archive file (${isIos ? 'ipa' : 'aab or apk'}):`,
|
||||
initial: defaultArchivePath,
|
||||
type: 'text',
|
||||
validate: async (path: string): Promise<boolean | string> => {
|
||||
|
||||
@@ -14,17 +14,19 @@ export interface ArchiveSource {
|
||||
export interface Archive {
|
||||
location: string;
|
||||
type: ArchiveType;
|
||||
realFileSource: ArchiveFileSource;
|
||||
}
|
||||
|
||||
export async function getArchiveAsync(
|
||||
platform: SubmissionPlatform,
|
||||
source: ArchiveSource
|
||||
): Promise<Archive> {
|
||||
const location = await getArchiveFileLocationAsync(source.archiveFile);
|
||||
const { location, realSource } = await getArchiveFileLocationAsync(source.archiveFile);
|
||||
const type = await getArchiveTypeAsync(platform, source.archiveType, location);
|
||||
return {
|
||||
location,
|
||||
type,
|
||||
realFileSource: realSource,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ export async function ensureAppStoreConnectAppExistsAsync(
|
||||
const { bundleIdentifier, appName, language } = ctx.commandFlags;
|
||||
|
||||
const resolvedBundleId =
|
||||
bundleIdentifier ?? (await getAppIdentifierAsync(ctx.projectDir, Platform.iOS));
|
||||
bundleIdentifier ??
|
||||
(await getAppIdentifierAsync({ projectDir: ctx.projectDir, platform: Platform.IOS, exp }));
|
||||
if (!resolvedBundleId) {
|
||||
throw new Error(
|
||||
`Please define "expo.ios.bundleIdentifier" in your app config or provide it using --bundle-identifier param.
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log, { learnMore } from '../../log';
|
||||
import BaseSubmitter from '../BaseSubmitter';
|
||||
import { SubmissionStatus } from '../SubmissionService.types';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
|
||||
import { IosSubmissionContext, SubmissionPlatform } from '../types';
|
||||
import { printSummary } from '../utils/summary';
|
||||
import {
|
||||
ArchiveSourceSummaryFields,
|
||||
formatArchiveSourceSummary,
|
||||
printSummary,
|
||||
} from '../utils/summary';
|
||||
import {
|
||||
AppSpecificPasswordSource,
|
||||
getAppSpecificPasswordAsync,
|
||||
@@ -21,6 +27,9 @@ interface ResolvedSourceOptions {
|
||||
appSpecificPassword: string;
|
||||
}
|
||||
|
||||
type SummaryData = Pick<IosSubmissionOptions, 'ascAppId' | 'appleId' | 'projectId'> &
|
||||
ArchiveSourceSummaryFields;
|
||||
|
||||
class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOptions> {
|
||||
protected readonly appStoreName: string = 'Apple App Store';
|
||||
|
||||
@@ -35,9 +44,26 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
resolvedSourceOptions
|
||||
);
|
||||
|
||||
printSummaryIOS(submissionConfig);
|
||||
printSummary(
|
||||
this.prepareSummaryData(this.options, resolvedSourceOptions),
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
const result = await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
|
||||
|
||||
await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
|
||||
if (result === SubmissionStatus.FINISHED) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
chalk.bold('Your binary has been successfully uploaded to App Store Connect!\n') +
|
||||
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.\n' +
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.\n' +
|
||||
'- When it’s done, you can see your build here' +
|
||||
learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${this.options.ascAppId}/appstore/ios`,
|
||||
{ learnMoreMessage: '' }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveSourceOptions(): Promise<ResolvedSourceOptions> {
|
||||
@@ -58,48 +84,40 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
): Promise<IosSubmissionConfig> {
|
||||
const { projectId, appleId, ascAppId } = options;
|
||||
const submissionConfig = {
|
||||
archiveUrl: archive.location,
|
||||
appleId,
|
||||
appSpecificPassword,
|
||||
appAppleId: ascAppId, //ASC App ID is called "appAppleId" on server side
|
||||
appleId,
|
||||
projectId,
|
||||
archiveUrl: archive.location,
|
||||
appSpecificPassword,
|
||||
};
|
||||
return submissionConfig;
|
||||
}
|
||||
|
||||
private prepareSummaryData(
|
||||
options: IosSubmissionOptions,
|
||||
{ archive }: ResolvedSourceOptions
|
||||
): SummaryData {
|
||||
const { appleId, ascAppId, projectId } = options;
|
||||
|
||||
// structuring order affects table rows order
|
||||
return {
|
||||
ascAppId,
|
||||
appleId,
|
||||
projectId,
|
||||
...formatArchiveSourceSummary(archive),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the summary as a table. Exported for testing locally.
|
||||
* @example
|
||||
* printSummaryIOS({
|
||||
* appleId: '[email protected]',
|
||||
* appSpecificPassword: 'Apple Pie^',
|
||||
* appAppleId: '1234567890',
|
||||
* projectId: '863f9337-65d2-40c6-acb3-c1054c5c09f8',
|
||||
* archiveUrl: 'https://turtle-v2-artifacts.s3.amazonaws.com/ios/6420592d-5b5d-439b-aed4-ccd278647138-ca4145d8468947df9ded737248a1a238.ipa',
|
||||
* });
|
||||
* @param submissionConfig
|
||||
*/
|
||||
export function printSummaryIOS(submissionConfig: IosSubmissionConfig) {
|
||||
const { appSpecificPassword, projectId, archiveUrl, ...logConfig } = submissionConfig;
|
||||
|
||||
printSummary(
|
||||
{ ...logConfig, projectId, archiveUrl },
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
}
|
||||
|
||||
const SummaryHumanReadableKeys: Record<keyof IosSubmissionConfig, string> = {
|
||||
const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
|
||||
ascAppId: 'ASC App ID',
|
||||
appleId: 'Apple ID',
|
||||
archiveUrl: 'Download URL',
|
||||
appSpecificPassword: 'Apple app-specific password',
|
||||
appAppleId: 'ASC App ID',
|
||||
projectId: 'Project ID',
|
||||
archiveUrl: 'Archive URL',
|
||||
archivePath: 'Archive Path',
|
||||
buildId: 'Build ID',
|
||||
};
|
||||
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof IosSubmissionConfig, Function>> = {
|
||||
appSpecificPassword: () => chalk.italic('[hidden]'),
|
||||
};
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof SummaryData, Function>> = {};
|
||||
|
||||
export default IosSubmitter;
|
||||
@@ -26,7 +26,7 @@ async function moveFileOfTypeAsync(
|
||||
extension: string,
|
||||
dest: string
|
||||
): Promise<string> {
|
||||
const [matching] = await glob(`*.${extension}`, {
|
||||
const [matching] = await glob(`**/*.${extension}`, {
|
||||
absolute: true,
|
||||
cwd: directory,
|
||||
});
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { Archive, ArchiveFileSourceType } from '../archiveSource';
|
||||
|
||||
export interface ArchiveSourceSummaryFields {
|
||||
archiveUrl?: string;
|
||||
archivePath?: string;
|
||||
buildId?: string;
|
||||
}
|
||||
|
||||
export function formatArchiveSourceSummary({
|
||||
realFileSource,
|
||||
}: Archive): ArchiveSourceSummaryFields {
|
||||
const summarySlice: ArchiveSourceSummaryFields = {};
|
||||
|
||||
switch (realFileSource.sourceType) {
|
||||
case ArchiveFileSourceType.path:
|
||||
summarySlice.archivePath = realFileSource.path;
|
||||
break;
|
||||
case ArchiveFileSourceType.url:
|
||||
summarySlice.archiveUrl = realFileSource.url;
|
||||
break;
|
||||
case ArchiveFileSourceType.buildId:
|
||||
summarySlice.buildId = realFileSource.id;
|
||||
break;
|
||||
case ArchiveFileSourceType.latest:
|
||||
// TODO: Resolve real build ID here
|
||||
summarySlice.buildId = '[latest]';
|
||||
break;
|
||||
}
|
||||
return summarySlice;
|
||||
}
|
||||
|
||||
export function printSummary<T>(
|
||||
summary: T,
|
||||
keyMap: Record<keyof T, string>,
|
||||
valueRemap: Partial<Record<keyof T, Function>>
|
||||
): void {
|
||||
const padWidth = longestStringLength(Object.keys(summary).map(key => keyMap[key as keyof T]));
|
||||
|
||||
const tableFormat = (name: string, msg: string) =>
|
||||
`${chalk.bold.cyan(pad(`${name}:`, padWidth + 1))} ${msg}`;
|
||||
const fields = [];
|
||||
for (const [key, entryValue] of Object.entries(summary)) {
|
||||
const label = `${keyMap[key as keyof T]}:`;
|
||||
const value = valueRemap[key as keyof T]?.(entryValue) ?? entryValue;
|
||||
fields.push({ label, value });
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
for (const [key, value] of Object.entries(summary)) {
|
||||
const displayKey = keyMap[key as keyof T];
|
||||
const displayValue = valueRemap[key as keyof T]?.(value) ?? value;
|
||||
Log.log(tableFormat(displayKey, displayValue));
|
||||
}
|
||||
Log.log(formatFields(fields, { labelFormat: chalk.bold.cyan }));
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
|
||||
function pad(str: string, width: number): string {
|
||||
const len = Math.max(0, width - str.length);
|
||||
return str + Array(len + 1).join(' ');
|
||||
}
|
||||
|
||||
function longestStringLength(values: string[]): number {
|
||||
return values.reduce((max, option) => Math.max(max, option.length), 0);
|
||||
}
|
||||
@@ -26,3 +26,32 @@ export async function maybeRenameExistingFileAsync(projectDir: string, filename:
|
||||
await fs.rename(desiredFilePath, path.resolve(projectDir, getRenamedFilename(filename, num)));
|
||||
}
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) {
|
||||
return `0`;
|
||||
}
|
||||
let multiplier = 1;
|
||||
if (bytes < 1024 * multiplier) {
|
||||
return `${Math.floor(bytes)} B`;
|
||||
}
|
||||
multiplier *= 1024;
|
||||
if (bytes < 102.4 * multiplier) {
|
||||
return `${(bytes / multiplier).toFixed(1)} KB`;
|
||||
}
|
||||
if (bytes < 1024 * multiplier) {
|
||||
return `${Math.floor(bytes / 1024)} KB`;
|
||||
}
|
||||
multiplier *= 1024;
|
||||
if (bytes < 102.4 * multiplier) {
|
||||
return `${(bytes / multiplier).toFixed(1)} MB`;
|
||||
}
|
||||
if (bytes < 1024 * multiplier) {
|
||||
return `${Math.floor(bytes / multiplier)} MB`;
|
||||
}
|
||||
multiplier *= 1024;
|
||||
if (bytes < 102.4 * multiplier) {
|
||||
return `${(bytes / multiplier).toFixed(1)} GB`;
|
||||
}
|
||||
return `${Math.floor(bytes / 1024)} GB`;
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
export default function formatFields(fields: { label: string; value: string }[]) {
|
||||
type FormatFieldsOptions = {
|
||||
labelFormat: (raw: string) => string;
|
||||
};
|
||||
|
||||
export default function formatFields(
|
||||
fields: { label: string; value: string }[],
|
||||
options: FormatFieldsOptions = { labelFormat: chalk.dim }
|
||||
) {
|
||||
const columnWidth = fields.reduce((a, b) => (a.label.length > b.label.length ? a : b)).label
|
||||
.length;
|
||||
|
||||
return fields
|
||||
.map(({ label, value }) => {
|
||||
let line = '';
|
||||
|
||||
line += chalk.dim(
|
||||
// make all labels fixed-width
|
||||
const formattedLabel = options.labelFormat(
|
||||
label.length < columnWidth ? `${label}${' '.repeat(columnWidth - label.length)}` : label
|
||||
);
|
||||
|
||||
line += ' ';
|
||||
line += value;
|
||||
|
||||
return line;
|
||||
return `${formattedLabel} ${value}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
@@ -4,20 +4,25 @@ interface GitStatusOptions {
|
||||
showUntracked?: boolean;
|
||||
}
|
||||
|
||||
async function gitStatusAsync({ showUntracked }: GitStatusOptions = {}): Promise<string> {
|
||||
export async function gitStatusAsync({ showUntracked }: GitStatusOptions = {}): Promise<string> {
|
||||
return (await spawnAsync('git', ['status', '-s', showUntracked ? '-uall' : '-uno'])).stdout;
|
||||
}
|
||||
|
||||
async function gitDiffAsync({ withPager = false }: { withPager?: boolean } = {}): Promise<void> {
|
||||
export async function gitDiffAsync({ withPager = false }: { withPager?: boolean } = {}): Promise<
|
||||
void
|
||||
> {
|
||||
const options = withPager ? [] : ['--no-pager'];
|
||||
await spawnAsync('git', [...options, 'diff'], { stdio: ['ignore', 'inherit', 'inherit'] });
|
||||
}
|
||||
|
||||
async function getGitDiffOutputAsync(): Promise<string> {
|
||||
export async function getGitDiffOutputAsync(): Promise<string> {
|
||||
return (await spawnAsync('git', ['--no-pager', 'diff'])).stdout;
|
||||
}
|
||||
|
||||
async function gitAddAsync(file: string, options?: { intentToAdd?: boolean }): Promise<void> {
|
||||
export async function gitAddAsync(
|
||||
file: string,
|
||||
options?: { intentToAdd?: boolean }
|
||||
): Promise<void> {
|
||||
if (options?.intentToAdd) {
|
||||
await spawnAsync('git', ['add', '--intent-to-add', file]);
|
||||
} else {
|
||||
@@ -25,11 +30,11 @@ async function gitAddAsync(file: string, options?: { intentToAdd?: boolean }): P
|
||||
}
|
||||
}
|
||||
|
||||
async function gitRootDirectoryAsync(): Promise<string> {
|
||||
export async function gitRootDirectoryAsync(): Promise<string> {
|
||||
return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
|
||||
}
|
||||
|
||||
async function doesGitRepoExistAsync(): Promise<boolean> {
|
||||
export async function doesGitRepoExistAsync(): Promise<boolean> {
|
||||
try {
|
||||
await spawnAsync('git', ['rev-parse', '--git-dir']);
|
||||
return true;
|
||||
@@ -38,7 +43,15 @@ async function doesGitRepoExistAsync(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function isGitInstalledAsync(): Promise<boolean> {
|
||||
export async function gitCommitHashAsync(): Promise<string | undefined> {
|
||||
try {
|
||||
return (await spawnAsync('git', ['rev-parse', 'HEAD'])).stdout.trim();
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isGitInstalledAsync(): Promise<boolean> {
|
||||
try {
|
||||
await spawnAsync('git', ['--help']);
|
||||
} catch (error) {
|
||||
@@ -50,7 +63,7 @@ async function isGitInstalledAsync(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getBranchNameAsync(): Promise<string | null> {
|
||||
export async function getBranchNameAsync(): Promise<string | null> {
|
||||
try {
|
||||
return (await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout.trim();
|
||||
} catch (e) {
|
||||
@@ -58,22 +71,10 @@ async function getBranchNameAsync(): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
async function getLastCommitMessageAsync(): Promise<string | null> {
|
||||
export async function getLastCommitMessageAsync(): Promise<string | null> {
|
||||
try {
|
||||
return (await spawnAsync('git', ['--no-pager', 'log', '-1', '--pretty=%B'])).stdout.trim();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
gitStatusAsync,
|
||||
gitDiffAsync,
|
||||
getGitDiffOutputAsync,
|
||||
gitAddAsync,
|
||||
doesGitRepoExistAsync,
|
||||
gitRootDirectoryAsync,
|
||||
isGitInstalledAsync,
|
||||
getBranchNameAsync,
|
||||
getLastCommitMessageAsync,
|
||||
};
|
||||
@@ -16,8 +16,8 @@ function createProgressTracker({
|
||||
completedMessage,
|
||||
}: {
|
||||
total?: number;
|
||||
message: string;
|
||||
completedMessage?: string;
|
||||
message: string | ((ratio: number) => string);
|
||||
completedMessage: string;
|
||||
}): ProgressHandler {
|
||||
let bar: ora.Ora | null = null;
|
||||
let calcTotal: number = total ?? 0;
|
||||
@@ -26,17 +26,17 @@ function createProgressTracker({
|
||||
|
||||
const timerLabel = String(Date.now());
|
||||
|
||||
const withPercent = (v: number) => {
|
||||
const getMessage = (v: number) => {
|
||||
const ratio = Math.min(Math.max(v, 0), 1);
|
||||
const percent = Math.floor(ratio * 100);
|
||||
return `${message} ${percent.toFixed(0)}%`;
|
||||
return typeof message === 'string' ? `${message} ${percent.toFixed(0)}%` : message(ratio);
|
||||
};
|
||||
|
||||
return ({ progress, isComplete, error }) => {
|
||||
if (progress) {
|
||||
if (!bar && (progress.total !== undefined || total !== undefined)) {
|
||||
calcTotal = (total ?? progress.total) as number;
|
||||
bar = ora(withPercent(0)).start();
|
||||
bar = ora(getMessage(0)).start();
|
||||
startTimer(timerLabel);
|
||||
}
|
||||
if (progress.total) {
|
||||
@@ -51,7 +51,7 @@ function createProgressTracker({
|
||||
percentage = current / calcTotal;
|
||||
}
|
||||
|
||||
bar.text = withPercent(percentage);
|
||||
bar.text = getMessage(percentage);
|
||||
}
|
||||
transferredSoFar = progress.transferred;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ function createProgressTracker({
|
||||
if (error) {
|
||||
bar.fail();
|
||||
} else if (isComplete) {
|
||||
bar.succeed(`${completedMessage ?? message} ${chalk.dim(prettyTime)}`);
|
||||
bar.succeed(`${completedMessage} ${chalk.dim(prettyTime)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with the eas.json",
|
||||
"version": "0.4.3",
|
||||
"version": "0.7.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "0.2.12",
|
||||
"@expo/eas-build-job": "0.2.21",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"tslib": "^1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Android, Workflow, iOS } from '@expo/eas-build-job';
|
||||
import { Android, Cache, Ios, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
export enum CredentialsSource {
|
||||
LOCAL = 'local',
|
||||
@@ -13,41 +13,46 @@ export type DistributionType = AndroidDistributionType | iOSDistributionType;
|
||||
export type VersionAutoIncrement = boolean | 'version' | 'buildNumber';
|
||||
|
||||
export interface AndroidManagedBuildProfile extends Android.BuilderEnvironment {
|
||||
workflow: Workflow.Managed;
|
||||
workflow: Workflow.MANAGED;
|
||||
credentialsSource: CredentialsSource;
|
||||
buildType?: Android.ManagedBuildType;
|
||||
releaseChannel?: string;
|
||||
distribution?: AndroidDistributionType;
|
||||
cache: Cache | null;
|
||||
}
|
||||
|
||||
export interface AndroidGenericBuildProfile extends Android.BuilderEnvironment {
|
||||
workflow: Workflow.Generic;
|
||||
workflow: Workflow.GENERIC;
|
||||
credentialsSource: CredentialsSource;
|
||||
gradleCommand?: string;
|
||||
releaseChannel?: string;
|
||||
artifactPath?: string;
|
||||
withoutCredentials?: boolean;
|
||||
distribution?: AndroidDistributionType;
|
||||
cache: Cache | null;
|
||||
}
|
||||
|
||||
export interface iOSManagedBuildProfile extends iOS.BuilderEnvironment {
|
||||
workflow: Workflow.Managed;
|
||||
export interface iOSManagedBuildProfile extends Ios.BuilderEnvironment {
|
||||
workflow: Workflow.MANAGED;
|
||||
credentialsSource: CredentialsSource;
|
||||
buildType?: iOS.ManagedBuildType;
|
||||
buildType?: Ios.ManagedBuildType;
|
||||
releaseChannel?: string;
|
||||
distribution?: iOSDistributionType;
|
||||
autoIncrement: VersionAutoIncrement;
|
||||
cache: Cache | null;
|
||||
}
|
||||
|
||||
export interface iOSGenericBuildProfile extends iOS.BuilderEnvironment {
|
||||
workflow: Workflow.Generic;
|
||||
export interface iOSGenericBuildProfile extends Ios.BuilderEnvironment {
|
||||
workflow: Workflow.GENERIC;
|
||||
credentialsSource: CredentialsSource;
|
||||
scheme?: string;
|
||||
schemeBuildConfiguration?: iOS.SchemeBuildConfiguration;
|
||||
schemeBuildConfiguration?: Ios.SchemeBuildConfiguration | 'Auto';
|
||||
releaseChannel?: string;
|
||||
artifactPath?: string;
|
||||
distribution?: iOSDistributionType;
|
||||
autoIncrement: VersionAutoIncrement;
|
||||
cache: Cache | null;
|
||||
disableIosBundleIdentifierValidation?: boolean;
|
||||
}
|
||||
|
||||
export type AndroidBuildProfile = AndroidManagedBuildProfile | AndroidGenericBuildProfile;
|
||||
|
||||
@@ -6,6 +6,9 @@ import { AndroidBuildProfile, BuildProfile, EasConfig, iOSBuildProfile } from '.
|
||||
import { EasJsonSchema, schemaBuildProfileMap } from './EasJsonSchema';
|
||||
|
||||
interface EasJson {
|
||||
experimental?: {
|
||||
disableIosBundleIdentifierValidation?: boolean;
|
||||
};
|
||||
builds: {
|
||||
android?: { [key: string]: BuildProfilePreValidation };
|
||||
ios?: { [key: string]: BuildProfilePreValidation };
|
||||
@@ -26,7 +29,7 @@ export class EasJsonReader {
|
||||
let androidConfig;
|
||||
if (['android', 'all'].includes(this.platform)) {
|
||||
androidConfig = this.validateBuildProfile<AndroidBuildProfile>(
|
||||
Platform.Android,
|
||||
Platform.ANDROID,
|
||||
buildProfileName,
|
||||
easJson.builds?.android || {}
|
||||
);
|
||||
@@ -34,15 +37,21 @@ export class EasJsonReader {
|
||||
let iosConfig;
|
||||
if (['ios', 'all'].includes(this.platform)) {
|
||||
iosConfig = this.validateBuildProfile<iOSBuildProfile>(
|
||||
Platform.iOS,
|
||||
Platform.IOS,
|
||||
buildProfileName,
|
||||
easJson.builds?.ios || {}
|
||||
);
|
||||
}
|
||||
const iosExperimental = easJson.experimental?.disableIosBundleIdentifierValidation
|
||||
? {
|
||||
disableIosBundleIdentifierValidation:
|
||||
easJson.experimental.disableIosBundleIdentifierValidation,
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
builds: {
|
||||
...(androidConfig ? { android: androidConfig } : {}),
|
||||
...(iosConfig ? { ios: iosConfig } : {}),
|
||||
...(iosConfig ? { ios: { ...iosConfig, ...iosExperimental } } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -53,8 +62,8 @@ export class EasJsonReader {
|
||||
const androidProfiles = easJson.builds?.android ?? {};
|
||||
for (const name of Object.keys(androidProfiles)) {
|
||||
try {
|
||||
if (this.isWorkflowKeySpecified(Platform.Android, name, androidProfiles)) {
|
||||
await this.validateBuildProfile(Platform.Android, name, androidProfiles);
|
||||
if (this.isWorkflowKeySpecified(Platform.ANDROID, name, androidProfiles)) {
|
||||
await this.validateBuildProfile(Platform.ANDROID, name, androidProfiles);
|
||||
}
|
||||
} catch (err) {
|
||||
err.msg = `Failed to validate Android build profile "${name}"\n${err.msg}`;
|
||||
@@ -64,8 +73,8 @@ export class EasJsonReader {
|
||||
const iosProfiles = easJson.builds?.ios ?? {};
|
||||
for (const name of Object.keys(iosProfiles)) {
|
||||
try {
|
||||
if (this.isWorkflowKeySpecified(Platform.iOS, name, iosProfiles)) {
|
||||
await this.validateBuildProfile(Platform.iOS, name, iosProfiles);
|
||||
if (this.isWorkflowKeySpecified(Platform.IOS, name, iosProfiles)) {
|
||||
await this.validateBuildProfile(Platform.IOS, name, iosProfiles);
|
||||
}
|
||||
} catch (err) {
|
||||
err.msg = `Failed to validate iOS build profile "${name}"\n${err.msg}`;
|
||||
@@ -94,7 +103,7 @@ export class EasJsonReader {
|
||||
buildProfiles: Record<string, BuildProfilePreValidation>
|
||||
): T {
|
||||
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
|
||||
if (![Workflow.Generic, Workflow.Managed].includes(buildProfile.workflow)) {
|
||||
if (![Workflow.GENERIC, Workflow.MANAGED].includes(buildProfile.workflow)) {
|
||||
throw new Error(
|
||||
'"workflow" key is required in a build profile and has to be one of ["generic", "managed"].'
|
||||
);
|
||||
@@ -141,15 +150,14 @@ export class EasJsonReader {
|
||||
if (!buildProfile) {
|
||||
throw new Error(`There is no profile named ${buildProfileName} for platform ${platform}`);
|
||||
}
|
||||
const baseProfileName = buildProfile.extends;
|
||||
delete buildProfile.extends;
|
||||
const { extends: baseProfileName, ...buildProfileRest } = buildProfile;
|
||||
if (baseProfileName) {
|
||||
return deepMerge(
|
||||
this.resolveBuildProfile(platform, baseProfileName, buildProfiles, depth + 1),
|
||||
buildProfile
|
||||
buildProfileRest
|
||||
);
|
||||
} else {
|
||||
return buildProfile;
|
||||
return buildProfileRest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Android, iOS } from '@expo/eas-build-job';
|
||||
import { Android, Ios } from '@expo/eas-build-job';
|
||||
import Joi, { CustomHelpers } from '@hapi/joi';
|
||||
|
||||
const semverSchemaCheck = (value: any, helpers: CustomHelpers) => {
|
||||
@@ -13,21 +13,28 @@ const AndroidBuilderEnvironmentSchema = Joi.object({
|
||||
image: Joi.string()
|
||||
.valid(...Android.builderBaseImages)
|
||||
.default('default'),
|
||||
node: Joi.string().custom(semverSchemaCheck),
|
||||
yarn: Joi.string().custom(semverSchemaCheck),
|
||||
ndk: Joi.string(),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string()).default({}),
|
||||
node: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
|
||||
});
|
||||
|
||||
const IosBuilderEnvironmentSchema = Joi.object({
|
||||
image: Joi.string()
|
||||
.valid(...iOS.builderBaseImages)
|
||||
.valid(...Ios.builderBaseImages)
|
||||
.default('default'),
|
||||
node: Joi.string().custom(semverSchemaCheck),
|
||||
yarn: Joi.string().custom(semverSchemaCheck),
|
||||
fastlane: Joi.string().custom(semverSchemaCheck),
|
||||
cocoapods: Joi.string().custom(semverSchemaCheck),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string()).default({}),
|
||||
node: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
bundler: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
|
||||
});
|
||||
|
||||
const CacheSchema = Joi.object({
|
||||
key: Joi.string().allow('').default('').max(128),
|
||||
cacheDefaultPaths: Joi.boolean().default(true),
|
||||
customPaths: Joi.array().items(Joi.string()).default([]),
|
||||
});
|
||||
|
||||
const AndroidGenericSchema = Joi.object({
|
||||
@@ -40,8 +47,9 @@ const AndroidGenericSchema = Joi.object({
|
||||
}),
|
||||
releaseChannel: Joi.string(),
|
||||
artifactPath: Joi.string(),
|
||||
withoutCredentials: Joi.boolean(),
|
||||
withoutCredentials: Joi.boolean().default(false),
|
||||
distribution: Joi.string().valid('store', 'internal').default('store'),
|
||||
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
|
||||
}).concat(AndroidBuilderEnvironmentSchema);
|
||||
|
||||
const AndroidManagedSchema = Joi.object({
|
||||
@@ -54,30 +62,33 @@ const AndroidManagedSchema = Joi.object({
|
||||
otherwise: Joi.string().valid('apk', 'app-bundle', 'development-client').default('app-bundle'),
|
||||
}),
|
||||
distribution: Joi.string().valid('store', 'internal').default('store'),
|
||||
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
|
||||
}).concat(AndroidBuilderEnvironmentSchema);
|
||||
|
||||
const iOSGenericSchema = Joi.object({
|
||||
workflow: Joi.string().valid('generic').required(),
|
||||
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
|
||||
scheme: Joi.string(),
|
||||
schemeBuildConfiguration: Joi.string().valid('Debug', 'Release'),
|
||||
schemeBuildConfiguration: Joi.string().valid('Debug', 'Release', 'Auto').default('Release'),
|
||||
releaseChannel: Joi.string(),
|
||||
artifactPath: Joi.string(),
|
||||
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
|
||||
autoIncrement: Joi.alternatives()
|
||||
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
|
||||
.default(false),
|
||||
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
|
||||
}).concat(IosBuilderEnvironmentSchema);
|
||||
|
||||
const iOSManagedSchema = Joi.object({
|
||||
workflow: Joi.string().valid('managed').required(),
|
||||
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
|
||||
buildType: Joi.string().valid('release', 'development-client'),
|
||||
buildType: Joi.string().valid('release', 'development-client').default('release'),
|
||||
releaseChannel: Joi.string(),
|
||||
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
|
||||
autoIncrement: Joi.alternatives()
|
||||
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
|
||||
.default(false),
|
||||
cache: CacheSchema.allow(null).default({ key: '', cacheDefaultPaths: true, customPaths: [] }),
|
||||
}).concat(IosBuilderEnvironmentSchema);
|
||||
|
||||
const schemaBuildProfileMap: Record<string, Record<string, Joi.Schema>> = {
|
||||
@@ -92,6 +103,9 @@ const schemaBuildProfileMap: Record<string, Record<string, Joi.Schema>> = {
|
||||
};
|
||||
|
||||
const EasJsonSchema = Joi.object({
|
||||
experimental: Joi.object({
|
||||
disableIosBundleIdentifierValidation: Joi.boolean(),
|
||||
}),
|
||||
builds: Joi.object({
|
||||
android: Joi.object().pattern(
|
||||
Joi.string(),
|
||||
|
||||
@@ -28,6 +28,8 @@ test('minimal valid android eas.json', async () => {
|
||||
distribution: 'store',
|
||||
credentialsSource: 'auto',
|
||||
env: {},
|
||||
withoutCredentials: false,
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
image: 'default',
|
||||
},
|
||||
},
|
||||
@@ -53,7 +55,9 @@ test('minimal valid ios eas.json', async () => {
|
||||
workflow: 'generic',
|
||||
autoIncrement: false,
|
||||
env: {},
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
image: 'default',
|
||||
schemeBuildConfiguration: 'Release',
|
||||
},
|
||||
},
|
||||
}).toEqual(easJson);
|
||||
@@ -80,6 +84,8 @@ test('minimal valid eas.json for both platforms', async () => {
|
||||
distribution: 'store',
|
||||
credentialsSource: 'auto',
|
||||
env: {},
|
||||
withoutCredentials: false,
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
image: 'default',
|
||||
},
|
||||
ios: {
|
||||
@@ -88,7 +94,9 @@ test('minimal valid eas.json for both platforms', async () => {
|
||||
credentialsSource: 'auto',
|
||||
autoIncrement: false,
|
||||
env: {},
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
image: 'default',
|
||||
schemeBuildConfiguration: 'Release',
|
||||
},
|
||||
},
|
||||
}).toEqual(easJson);
|
||||
@@ -115,6 +123,8 @@ test('valid eas.json with both platform, but reading only android', async () =>
|
||||
distribution: 'store',
|
||||
credentialsSource: 'auto',
|
||||
env: {},
|
||||
withoutCredentials: false,
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
image: 'default',
|
||||
},
|
||||
},
|
||||
@@ -148,6 +158,7 @@ test('valid eas.json for development client builds', async () => {
|
||||
distribution: 'store',
|
||||
env: {},
|
||||
image: 'default',
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
buildType: 'development-client',
|
||||
},
|
||||
ios: {
|
||||
@@ -157,6 +168,7 @@ test('valid eas.json for development client builds', async () => {
|
||||
autoIncrement: false,
|
||||
env: {},
|
||||
image: 'default',
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
buildType: 'development-client',
|
||||
},
|
||||
},
|
||||
@@ -185,6 +197,8 @@ test('valid generic profile for internal distribution on Android', async () => {
|
||||
credentialsSource: 'auto',
|
||||
gradleCommand: ':app:assembleRelease',
|
||||
env: {},
|
||||
withoutCredentials: false,
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
image: 'default',
|
||||
},
|
||||
},
|
||||
@@ -213,6 +227,7 @@ test('valid managed profile for internal distribution on Android', async () => {
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'auto',
|
||||
env: {},
|
||||
cache: { key: '', cacheDefaultPaths: true, customPaths: [] },
|
||||
image: 'default',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1093,10 +1093,10 @@
|
||||
"@babel/preset-env" "^7.4.4"
|
||||
"@babel/preset-typescript" "^7.3.3"
|
||||
|
||||
"@expo/[email protected]9-alpha.0":
|
||||
version "1.0.19-alpha.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-1.0.19-alpha.0.tgz#3d57c266bfae0c7b310ac99e9949c9d03120c103"
|
||||
integrity sha512-mOPIvi1tanMftgeEjDB4vckLKY5dKdJ80R2cg27YyaP4IS1pYGaHtJKBKb6Pr1w5xvyRxd2AlX6C7L8lEzTBaw==
|
||||
"@expo/[email protected].21-alpha.0":
|
||||
version "1.0.21-alpha.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-1.0.21-alpha.0.tgz#cc6d428809a8b1f483de1f4ca6b8219308bb3680"
|
||||
integrity sha512-npi9sttSQOxl/NWc/FSlIy+ws9HgUoX3vsbu+rgTmD2ARtF5D8x5NR5upnWWpwoq6ZXaYXwDXNcKZHWLGrCfjA==
|
||||
dependencies:
|
||||
"@expo/config-types" "^40.0.0-beta.2"
|
||||
"@expo/configure-splash-screen" "0.3.4"
|
||||
@@ -1151,10 +1151,10 @@
|
||||
xcode "^3.0.0"
|
||||
xml-js "^1.6.11"
|
||||
|
||||
"@expo/[email protected].12":
|
||||
version "0.2.12"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.12.tgz#35224e8ad7e9d6f70d76172b75c3b6824293470e"
|
||||
integrity sha512-a70/NZfsnQFnEqt+b6DK8g1OAExRjHSDf4MFOVAXg7DlhIIB9cAhRiJE/5ksfZfaJ7CI/RdEysU7fCWkspcNig==
|
||||
"@expo/[email protected]1":
|
||||
version "0.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.21.tgz#86c4d69d36dc4efc7a23fea97c8d6a73f27295de"
|
||||
integrity sha512-H0yNXmeiLQ382gqSRBbAruew6ptmM3TPzWNPXc9yIxj0dPLOBhezu2LV7H+n1BcNp15aWeZIRb4e95vs4iy78Q==
|
||||
dependencies:
|
||||
"@hapi/joi" "^17.1.1"
|
||||
|
||||
|
||||
Reference in new issue
Block a user