mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9021887072 | ||
|
|
6a80d4c8e9 | ||
|
|
471e26817a | ||
|
|
14bbd1ee0f | ||
|
|
22b06b444d | ||
|
|
cebd29e40d | ||
|
|
acf7e55b5b | ||
|
|
7c29aa3b38 | ||
|
|
dee5b4cf60 | ||
|
|
896e14b1e4 | ||
|
|
42c20d6ef5 |
No files matched your search
+26
-3
@@ -6,7 +6,32 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require `eas submit` to be configured with eas.json submit profiles. Drop support for CLI params. ([#590](https://github.com/expo/eas-cli/pull/590) by [@dsokal](https://github.com/dsokal))
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed force-creation, app secret creation, and secret deletion mutations. ([#606](https://github.com/expo/eas-cli/pull/606) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.27.0](https://github.com/expo/eas-cli/releases/tag/v0.27.0) - 2021-09-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Implement support for `--platform all` in `eas submit`. ([#598](https://github.com/expo/eas-cli/pull/598) by [@dsokal](https://github.com/dsokal))
|
||||
- Implement support for `--non-interactive` in `eas submit`. ([#600](https://github.com/expo/eas-cli/pull/600) by [@dsokal](https://github.com/dsokal))
|
||||
- Add auto-submit feature. Run `eas build --auto-submit` to submit automatically on build complete. ([#603](https://github.com/expo/eas-cli/pull/603) by [@dsokal](https://github.com/dsokal))
|
||||
- Allow using env var for `android.serviceAccountKeyPath` in submit profiles. ([#604](https://github.com/expo/eas-cli/pull/604) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix detecting `googleServicesFile` on Windows. ([#602](https://github.com/expo/eas-cli/pull/602) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.26.0](https://github.com/expo/eas-cli/releases/tag/v0.26.0) - 2021-09-08
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require `eas submit` to be configured with eas.json submit profiles (see [https://docs.expo.dev/submit/eas-json/](https://docs.expo.dev/submit/eas-json/) for details). Drop support for CLI params. ([#590](https://github.com/expo/eas-cli/pull/590) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -17,8 +42,6 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Fix `eas build` from throwing an exception when detecting the git `core.ignorecase` setting. ([#592](https://github.com/expo/eas-cli/pull/592) by [@mwillbanks](https://github.com/mwillbanks))
|
||||
- Support app names that consist only of unicode characters. ([#596](https://github.com/expo/eas-cli/pull/596) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.25.0](https://github.com/expo/eas-cli/releases/tag/v0.25.0) - 2021-09-01
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.26.0"
|
||||
"version": "0.27.1"
|
||||
}
|
||||
+59
-41
@@ -73,7 +73,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/login.js)_
|
||||
_See code: [src/commands/account/login.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/account/login.js)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -87,7 +87,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/logout.js)_
|
||||
_See code: [src/commands/account/logout.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/account/logout.js)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -101,7 +101,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/account/view.js)_
|
||||
_See code: [src/commands/account/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/account/view.js)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -112,7 +112,7 @@ USAGE
|
||||
$ eas analytics [STATUS]
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/analytics.js)_
|
||||
_See code: [src/commands/analytics.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/analytics.js)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -124,16 +124,28 @@ USAGE
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios|all)
|
||||
--clear-cache Clear cache before the build
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run command in --non-interactive mode
|
||||
--profile=profile [default: release] Name of the build profile from eas.json
|
||||
--skip-project-configuration Skip project configuration
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
|
||||
--auto-submit Submit on build complete using the submit profile with the same name as the
|
||||
build profile
|
||||
|
||||
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
|
||||
|
||||
--clear-cache Clear cache before the build
|
||||
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
|
||||
--local Run build locally [experimental]
|
||||
|
||||
--non-interactive Run command in non-interactive mode
|
||||
|
||||
--profile=PROFILE_NAME [default: release] Name of the build profile from eas.json
|
||||
|
||||
--skip-project-configuration Skip project configuration
|
||||
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/index.js)_
|
||||
_See code: [src/commands/build/index.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/index.js)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -144,7 +156,7 @@ USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
_See code: [src/commands/build/cancel.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/cancel.js)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -158,7 +170,7 @@ OPTIONS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/configure.js)_
|
||||
_See code: [src/commands/build/configure.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/configure.js)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -191,7 +203,7 @@ OPTIONS
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/list.js)_
|
||||
_See code: [src/commands/build/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/list.js)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -205,7 +217,7 @@ OPTIONS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/build/view.js)_
|
||||
_See code: [src/commands/build/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/build/view.js)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -220,7 +232,7 @@ OPTIONS
|
||||
--profile=profile
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/config.js)_
|
||||
_See code: [src/commands/config.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/config.js)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -231,7 +243,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/credentials.js)_
|
||||
_See code: [src/commands/credentials.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/credentials.js)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -242,7 +254,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/create.js)_
|
||||
_See code: [src/commands/device/create.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/device/create.js)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -256,7 +268,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/list.js)_
|
||||
_See code: [src/commands/device/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/device/list.js)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -267,7 +279,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/device/view.js)_
|
||||
_See code: [src/commands/device/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/device/view.js)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -278,7 +290,7 @@ USAGE
|
||||
$ eas diagnostics
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
_See code: [src/commands/diagnostics.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/diagnostics.js)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -306,7 +318,7 @@ USAGE
|
||||
$ eas project:info
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/project/info.js)_
|
||||
_See code: [src/commands/project/info.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/project/info.js)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -320,7 +332,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/project/init.js)_
|
||||
_See code: [src/commands/project/init.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/project/init.js)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -337,7 +349,7 @@ OPTIONS
|
||||
--value=value Value of the secret
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/create.js)_
|
||||
_See code: [src/commands/secret/create.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/secret/create.js)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -354,7 +366,7 @@ DESCRIPTION
|
||||
Unsure where to find the secret's ID? Run eas secrets:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/secret/delete.js)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -365,7 +377,7 @@ USAGE
|
||||
$ eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/secret/list.js)_
|
||||
_See code: [src/commands/secret/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/secret/list.js)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -376,14 +388,20 @@ USAGE
|
||||
$ eas submit
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios)
|
||||
--id=id ID of the build to submit
|
||||
--latest Submit the latest build for specified platform
|
||||
--path=path Path to the .apk/.aab/.ipa file
|
||||
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in eas.json.
|
||||
--url=url App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--[no-]wait Wait for submission to complete
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=id ID of the build to submit
|
||||
--latest Submit the latest build for specified platform
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=path Path to the .apk/.aab/.ipa file
|
||||
|
||||
--profile=profile Name of the submit profile from eas.json. Defaults to "release" if defined in
|
||||
eas.json.
|
||||
|
||||
--url=url App archive url
|
||||
|
||||
--verbose Always print logs from Submission Service
|
||||
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
@@ -392,7 +410,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/submit.js)_
|
||||
_See code: [src/commands/submit.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/submit.js)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -411,7 +429,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
_See code: [src/commands/webhook/create.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/create.js)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -425,7 +443,7 @@ ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
_See code: [src/commands/webhook/delete.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/delete.js)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -439,7 +457,7 @@ OPTIONS
|
||||
--event=(BUILD) Event type that triggers the webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
_See code: [src/commands/webhook/list.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/list.js)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -459,7 +477,7 @@ OPTIONS
|
||||
--url=url Webhook URL
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
_See code: [src/commands/webhook/update.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/update.js)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -473,5 +491,5 @@ ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.26.0/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
_See code: [src/commands/webhook/view.js](https://github.com/expo/eas-cli/blob/v0.27.1/packages/eas-cli/src/commands/webhook/view.js)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.26.0",
|
||||
"version": "0.27.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -14,7 +14,7 @@
|
||||
"@expo/config": "3.3.42",
|
||||
"@expo/config-plugins": "3.1.0",
|
||||
"@expo/eas-build-job": "0.2.46",
|
||||
"@expo/eas-json": "^0.26.0",
|
||||
"@expo/eas-json": "^0.27.0",
|
||||
"@expo/json-file": "8.2.25",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.14",
|
||||
|
||||
@@ -9,13 +9,13 @@ import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/genera
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { requestedPlatformDisplayNames } from '../platform';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleep } from '../utils/promise';
|
||||
import vcs from '../vcs';
|
||||
import { requestedPlatformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { MetadataContext, collectMetadata } from './metadata';
|
||||
|
||||
@@ -6,6 +6,7 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
@@ -13,7 +14,6 @@ import vcs from '../vcs';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
import { RequestedPlatform } from './types';
|
||||
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
|
||||
|
||||
const configureCommitMessage = {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { RequestedPlatform } from './types';
|
||||
|
||||
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
|
||||
[RequestedPlatform.Ios]: 'iOS',
|
||||
[RequestedPlatform.Android]: 'Android',
|
||||
[RequestedPlatform.All]: 'Android and iOS',
|
||||
};
|
||||
@@ -5,13 +5,14 @@ import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { RequestedPlatform, TrackingContext } from './types';
|
||||
import { TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
|
||||
export interface ConfigureContext {
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
Ios = 'ios',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
NEW = 'new',
|
||||
IN_QUEUE = 'in-queue',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { requestedPlatformDisplayNames } from '../constants';
|
||||
import { requestedPlatformDisplayNames } from '../../platform';
|
||||
|
||||
export function logCredentialsSource(credentialsSource: CredentialsSource, platform: Platform) {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
if (!googleServicesFilePath) {
|
||||
return;
|
||||
}
|
||||
const rootDir = await vcs.getRootPathAsync();
|
||||
const rootDir = path.normalize(await vcs.getRootPathAsync());
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { configureAsync } from '../../build/configure';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
@@ -6,26 +6,40 @@ import {
|
||||
migrateAsync,
|
||||
} from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { error, exit } from '@oclif/errors';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { prepareAndroidBuildAsync } from '../../build/android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from '../../build/build';
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import { BuildContext, createBuildContextAsync } from '../../build/context';
|
||||
import { prepareIosBuildAsync } from '../../build/ios/build';
|
||||
import { RequestedPlatform } from '../../build/types';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { BuildFragment, BuildStatus } from '../../graphql/generated';
|
||||
import { BuildFragment, BuildStatus, SubmissionFragment } from '../../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../../platform';
|
||||
import {
|
||||
EAS_UNAVAILABLE_MESSAGE,
|
||||
isEasEnabledForProjectAsync,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../../project/metroConfig';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { createSubmissionContext } from '../../submissions/context';
|
||||
import {
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../../submissions/submit';
|
||||
import { printSubmissionDetailsUrls } from '../../submissions/utils/urls';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import vcs from '../../vcs';
|
||||
|
||||
@@ -39,6 +53,8 @@ interface RawBuildFlags {
|
||||
wait: boolean;
|
||||
'clear-cache': boolean;
|
||||
json: boolean;
|
||||
'auto-submit': boolean;
|
||||
'auto-submit-with-profile'?: string;
|
||||
}
|
||||
|
||||
interface BuildFlags {
|
||||
@@ -50,13 +66,18 @@ interface BuildFlags {
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
static description = 'start a build';
|
||||
|
||||
static flags = {
|
||||
platform: flags.enum({ char: 'p', options: ['android', 'ios', 'all'] }),
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
'skip-credentials-check': flags.boolean({
|
||||
default: false,
|
||||
hidden: true,
|
||||
@@ -72,10 +93,11 @@ export default class Build extends EasCommand {
|
||||
profile: flags.string({
|
||||
default: 'release',
|
||||
description: 'Name of the build profile from eas.json',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
'non-interactive': flags.boolean({
|
||||
default: false,
|
||||
description: 'Run command in --non-interactive mode',
|
||||
description: 'Run command in non-interactive mode',
|
||||
}),
|
||||
local: flags.boolean({
|
||||
default: false,
|
||||
@@ -90,8 +112,21 @@ export default class Build extends EasCommand {
|
||||
default: false,
|
||||
description: 'Clear cache before the build',
|
||||
}),
|
||||
'auto-submit': flags.boolean({
|
||||
default: false,
|
||||
description:
|
||||
'Submit on build complete using the submit profile with the same name as the build profile',
|
||||
exclusive: ['auto-submit-with-profile'],
|
||||
}),
|
||||
'auto-submit-with-profile': flags.string({
|
||||
description: 'Submit on build complete using the submit profile with provided name',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
exclusive: ['auto-submit'],
|
||||
}),
|
||||
};
|
||||
|
||||
private metroConfigValidated = false;
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags: rawFlags } = this.parse(Build);
|
||||
if (rawFlags.json) {
|
||||
@@ -108,33 +143,16 @@ export default class Build extends EasCommand {
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir, requestedPlatform);
|
||||
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platformsToBuild = this.getPlatformsToBuild(requestedPlatform);
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
|
||||
const startedBuilds: BuildFragment[] = [];
|
||||
let metroConfigValidated = false;
|
||||
for (const platform of platformsToBuild) {
|
||||
const ctx = await createBuildContextAsync({
|
||||
buildProfileName: flags.profile,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: await easJsonReader.readBuildProfileAsync(platform, flags.profile),
|
||||
local: flags.local,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform,
|
||||
for (const platform of platforms) {
|
||||
const maybeBuild = await this.prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
platform,
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
});
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED && !metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(ctx);
|
||||
metroConfigValidated = true;
|
||||
}
|
||||
|
||||
if (!ctx.local && !(await isEasEnabledForProjectAsync(ctx.projectId))) {
|
||||
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
|
||||
}
|
||||
|
||||
const maybeBuild = await this.startBuildAsync(ctx);
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push(maybeBuild);
|
||||
}
|
||||
@@ -147,10 +165,38 @@ export default class Build extends EasCommand {
|
||||
printLogsUrls(startedBuilds);
|
||||
Log.newLine();
|
||||
|
||||
if (flags.wait) {
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
|
||||
printBuildResults(builds, flags.json);
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
for (const build of startedBuilds) {
|
||||
const submission = await this.prepareAndStartSubmissionAsync({
|
||||
projectDir,
|
||||
build,
|
||||
flags,
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
});
|
||||
submissions.push(submission);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
return;
|
||||
}
|
||||
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(build => build.id));
|
||||
printBuildResults(builds, flags.json);
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||
);
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
this.exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
} else {
|
||||
// the following function also exits with non zero code if any of the submissions failed
|
||||
await waitForSubmissionsToCompleteAsync(submissions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,10 +208,14 @@ export default class Build extends EasCommand {
|
||||
if (flags.json && !nonInteractive) {
|
||||
error('--json is allowed only when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
const requestedPlatform =
|
||||
(flags.platform as RequestedPlatform | undefined) ?? (await this.promptForPlatformAsync());
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
if (flags.local) {
|
||||
if (flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined) {
|
||||
// TODO: implement this
|
||||
error('Auto-submits are not yet supported when building locally', { exit: 1 });
|
||||
}
|
||||
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
error('Builds for multiple platforms are not supported with flag --local', { exit: 1 });
|
||||
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
|
||||
@@ -181,40 +231,65 @@ export default class Build extends EasCommand {
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
const profile = flags['profile'];
|
||||
return {
|
||||
requestedPlatform,
|
||||
skipProjectConfiguration: flags['skip-project-configuration'],
|
||||
profile: flags['profile'],
|
||||
profile,
|
||||
nonInteractive,
|
||||
local: flags['local'],
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
json: flags['json'],
|
||||
autoSubmit: flags['auto-submit'],
|
||||
submitProfile: flags['auto-submit-with-profile'] ?? profile,
|
||||
};
|
||||
}
|
||||
|
||||
private async promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Build for platforms',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{ title: 'All', value: RequestedPlatform.All },
|
||||
{ title: 'iOS', value: RequestedPlatform.Ios },
|
||||
{ title: 'Android', value: RequestedPlatform.Android },
|
||||
],
|
||||
private async prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
flags,
|
||||
moreBuilds,
|
||||
}: {
|
||||
projectDir: string;
|
||||
platform: Platform;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
}): Promise<BuildFragment | undefined> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const buildProfile = await easJsonReader.readBuildProfileAsync(platform, flags.profile);
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: flags.profile,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile,
|
||||
local: flags.local,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration: flags.skipProjectConfiguration,
|
||||
});
|
||||
return platform;
|
||||
}
|
||||
|
||||
private getPlatformsToBuild(requestedPlatform: RequestedPlatform): Platform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [Platform.ANDROID, Platform.IOS];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [Platform.ANDROID];
|
||||
} else {
|
||||
return [Platform.IOS];
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} build`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (buildCtx.workflow === Workflow.MANAGED && !this.metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
|
||||
this.metroConfigValidated = true;
|
||||
}
|
||||
|
||||
if (!buildCtx.local && !(await isEasEnabledForProjectAsync(buildCtx.projectId))) {
|
||||
error(EAS_UNAVAILABLE_MESSAGE, { exit: 1 });
|
||||
}
|
||||
|
||||
return await this.startBuildAsync(buildCtx);
|
||||
}
|
||||
|
||||
private async startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
@@ -227,12 +302,47 @@ export default class Build extends EasCommand {
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
private async prepareAndStartSubmissionAsync({
|
||||
projectDir,
|
||||
build,
|
||||
flags,
|
||||
moreBuilds,
|
||||
}: {
|
||||
projectDir: string;
|
||||
build: BuildFragment;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const platform = toPlatform(build.platform);
|
||||
const submitProfile = await easJsonReader.readSubmitProfileAsync(platform, flags.submitProfile);
|
||||
const submissionCtx = createSubmissionContext({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId: build.project.id,
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[build.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return await submitAsync(submissionCtx);
|
||||
}
|
||||
|
||||
private exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Command, flags } from '@oclif/command';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { BuildDistributionType, BuildStatus, RequestedPlatform } from '../../build/types';
|
||||
import { BuildDistributionType, BuildStatus } from '../../build/types';
|
||||
import { formatGraphQLBuild } from '../../build/utils/formatBuild';
|
||||
import {
|
||||
AppPlatform,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
|
||||
@@ -149,6 +149,7 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
);
|
||||
} else if (scope === EnvironmentSecretScope.ACCOUNT) {
|
||||
const ownerAccount = findAccountByName(actor.accounts, accountName);
|
||||
|
||||
if (!ownerAccount) {
|
||||
Log.warn(
|
||||
`Your account (${getActorDisplayName(actor)}) doesn't have access to the ${chalk.bold(
|
||||
@@ -159,11 +160,12 @@ export default class EnvironmentSecretCreate extends Command {
|
||||
}
|
||||
|
||||
if (force) {
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAcccountNameAsync(projectId);
|
||||
const existingSecrets = await EnvironmentSecretsQuery.byAccountNameAsync(ownerAccount.name);
|
||||
const existingSecret = existingSecrets.find(secret => secret.name === name);
|
||||
|
||||
if (existingSecret) {
|
||||
await EnvironmentSecretMutation.delete(existingSecret.id);
|
||||
|
||||
Log.withTick(
|
||||
`Deleting existing secret ${chalk.bold(name)} on account ${chalk.bold(
|
||||
ownerAccount.name
|
||||
|
||||
@@ -1,33 +1,48 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader, IosSubmitProfile } from '@expo/eas-json';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { flags } from '@oclif/command';
|
||||
import { exit } from '@oclif/errors';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
|
||||
import { SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../platform';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
selectRequestedPlatformAsync,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { isEasEnabledForProjectAsync, warnEasUnavailable } from '../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { promptAsync } from '../prompts';
|
||||
import AndroidSubmitCommand from '../submissions/android/AndroidSubmitCommand';
|
||||
import IosSubmitCommand from '../submissions/ios/IosSubmitCommand';
|
||||
import { SubmitArchiveFlags } from '../submissions/types';
|
||||
import { displayLogsAsync } from '../submissions/utils/logs';
|
||||
import { SubmitArchiveFlags, createSubmissionContext } from '../submissions/context';
|
||||
import { submitAsync, waitToCompleteAsync } from '../submissions/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submissions/utils/urls';
|
||||
import { waitForSubmissionsEndAsync } from '../submissions/utils/wait';
|
||||
|
||||
interface Flags {
|
||||
interface RawFlags {
|
||||
platform?: string;
|
||||
profile?: string;
|
||||
latest?: boolean;
|
||||
id?: string;
|
||||
path?: string;
|
||||
url?: string;
|
||||
verbose: boolean;
|
||||
wait: boolean;
|
||||
platform?: Platform;
|
||||
profile?: string;
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
export default class BuildSubmit extends EasCommand {
|
||||
interface Flags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile?: string;
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
verbose: boolean;
|
||||
wait: boolean;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
export default class Submit extends EasCommand {
|
||||
static description = `submit build archive to app store
|
||||
See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/submit/eas-json/', {
|
||||
learnMoreMessage: '',
|
||||
@@ -37,7 +52,7 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
static flags = {
|
||||
platform: flags.enum({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
options: ['android', 'ios', 'all'],
|
||||
}),
|
||||
profile: flags.string({
|
||||
description:
|
||||
@@ -46,7 +61,6 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
latest: flags.boolean({
|
||||
description: 'Submit the latest build for specified platform',
|
||||
exclusive: ['id', 'path', 'url'],
|
||||
default: false,
|
||||
}),
|
||||
id: flags.string({
|
||||
description: 'ID of the build to submit',
|
||||
@@ -69,13 +83,15 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
default: true,
|
||||
allowNo: true,
|
||||
}),
|
||||
'non-interactive': flags.boolean({
|
||||
default: false,
|
||||
description: 'Run command in non-interactive mode',
|
||||
}),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const flags = this.parseFlags();
|
||||
const platform =
|
||||
(flags.platform?.toUpperCase() as AppPlatform | undefined) ??
|
||||
(await this.promptForPlatformAsync());
|
||||
const { flags: rawFlags } = this.parse(Submit);
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
|
||||
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
@@ -88,114 +104,72 @@ See how to configure submits with eas.json: ${learnMore('https://docs.expo.dev/s
|
||||
}
|
||||
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
let iosSubmitProfile: IosSubmitProfile | null = null;
|
||||
if (platform === AppPlatform.Android) {
|
||||
const submitProfile = await easJsonReader.readSubmitProfileAsync(
|
||||
Platform.ANDROID,
|
||||
flags.profile
|
||||
);
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
for (const platform of platforms) {
|
||||
const profile = await easJsonReader.readSubmitProfileAsync(platform, flags.profile);
|
||||
const ctx = createSubmissionContext({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
profile: submitProfile,
|
||||
profile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
submissions.push(await command.runAsync());
|
||||
} else {
|
||||
iosSubmitProfile = await easJsonReader.readSubmitProfileAsync(Platform.IOS, flags.profile);
|
||||
const ctx = IosSubmitCommand.createContext({
|
||||
projectDir,
|
||||
projectId,
|
||||
profile: iosSubmitProfile,
|
||||
archiveFlags: flags.archiveFlags,
|
||||
});
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
submissions.push(await command.runAsync());
|
||||
|
||||
if (platforms.length > 1) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const submission = await submitAsync(ctx);
|
||||
submissions.push(submission);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
|
||||
if (flags.wait) {
|
||||
Log.newLine();
|
||||
const completedSubmissions = await waitForSubmissionsEndAsync(submissions);
|
||||
for (const submission of completedSubmissions) {
|
||||
if (completedSubmissions.length > 1) {
|
||||
Log.log(
|
||||
`${appPlatformEmojis[submission.platform]}${chalk.bold(
|
||||
`${appPlatformDisplayNames[submission.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (
|
||||
submission.platform === AppPlatform.Ios &&
|
||||
submission.status === SubmissionStatus.Finished
|
||||
) {
|
||||
const logMsg = [
|
||||
chalk.bold('Your binary has been successfully uploaded to App Store Connect!'),
|
||||
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.',
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.',
|
||||
// ascAppIdentifier should be always available for ios submissions but check it anyway
|
||||
submission.iosConfig?.ascAppIdentifier &&
|
||||
`- When it’s done, you can see your build here: ${learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${submission.iosConfig?.ascAppIdentifier}/appstore/ios`,
|
||||
{ learnMoreMessage: '' }
|
||||
)}`,
|
||||
].join('\n');
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(logMsg);
|
||||
}
|
||||
await displayLogsAsync(submission, { verbose: flags.verbose });
|
||||
if (completedSubmissions.length > 1) {
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
this.exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||
await waitToCompleteAsync(submissions, { verbose: flags.verbose });
|
||||
}
|
||||
}
|
||||
|
||||
private parseFlags(): Flags {
|
||||
private async sanitizeFlagsAsync(flags: RawFlags): Promise<Flags> {
|
||||
const {
|
||||
flags: { platform, profile, verbose, wait, ...archiveFlags },
|
||||
} = this.parse(BuildSubmit);
|
||||
|
||||
return {
|
||||
platform: platform as Platform,
|
||||
profile,
|
||||
platform,
|
||||
verbose,
|
||||
wait,
|
||||
profile,
|
||||
'non-interactive': nonInteractive,
|
||||
...archiveFlags
|
||||
} = flags;
|
||||
|
||||
if (!flags.platform && nonInteractive) {
|
||||
error('--platform is required when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
if (archiveFlags.id || archiveFlags.path || archiveFlags.url) {
|
||||
error(
|
||||
'--id, --path, and --url params are only supported when performing a single-platform submit',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
archiveFlags,
|
||||
requestedPlatform,
|
||||
verbose,
|
||||
wait,
|
||||
profile,
|
||||
nonInteractive,
|
||||
};
|
||||
}
|
||||
|
||||
private async promptForPlatformAsync(): Promise<AppPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Submit to platform',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
title: 'iOS',
|
||||
value: AppPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: AppPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
return platform;
|
||||
}
|
||||
|
||||
private exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(submissions: SubmissionFragment[]): void {
|
||||
const nonFinishedSubmissions = submissions.filter(
|
||||
({ status }) => status !== SubmissionStatus.Finished
|
||||
);
|
||||
if (nonFinishedSubmissions.length > 0) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,20 @@ import { jester as mockJester } from '../../../../credentials/__tests__/fixtures
|
||||
import { testKeystore } from '../../../__tests__/fixtures-android';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { askForUserProvidedAsync } from '../../../utils/promptForCredentials';
|
||||
import { generateRandomKeystoreAsync } from '../../utils/keystore';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { CreateKeystore } from '../CreateKeystore';
|
||||
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
jest.mock('../../../../project/ensureProjectExists');
|
||||
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
|
||||
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../../utils/promptForCredentials');
|
||||
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
|
||||
|
||||
describe('CreateKeystore', () => {
|
||||
beforeEach(() => {
|
||||
asMock(generateRandomKeystoreAsync).mockReset();
|
||||
});
|
||||
it('creates a keystore in Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: false });
|
||||
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
|
||||
@@ -19,7 +24,8 @@ describe('CreateKeystore', () => {
|
||||
await createKeystoreAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created on expo servers
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync).toHaveBeenCalled();
|
||||
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
|
||||
});
|
||||
it('creates a keystore by uploading', async () => {
|
||||
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
|
||||
@@ -29,11 +35,11 @@ describe('CreateKeystore', () => {
|
||||
await createKeystoreAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created on expo servers
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledWith(
|
||||
expect(ctx.android.createKeystoreAsync).toHaveBeenCalledWith(
|
||||
appLookupParams.account,
|
||||
expect.objectContaining(testKeystore)
|
||||
);
|
||||
expect(generateRandomKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({ nonInteractive: true });
|
||||
|
||||
+10
-2
@@ -1,3 +1,4 @@
|
||||
import { asMock } from '../../../../__tests__/utils';
|
||||
import {
|
||||
getNewAndroidApiMock,
|
||||
testAndroidBuildCredentialsFragment,
|
||||
@@ -6,14 +7,20 @@ import {
|
||||
import { jester as mockJester } from '../../../__tests__/fixtures-constants';
|
||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../../errors';
|
||||
import { generateRandomKeystoreAsync } from '../../utils/keystore';
|
||||
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
|
||||
import { SetupBuildCredentials } from '../SetupBuildCredentials';
|
||||
|
||||
jest.mock('../../../../project/ensureProjectExists');
|
||||
jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
|
||||
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
|
||||
|
||||
describe('SetupBuildCredentials', () => {
|
||||
beforeEach(() => {
|
||||
asMock(generateRandomKeystoreAsync).mockReset();
|
||||
});
|
||||
|
||||
it('skips setup when there are prior credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
nonInteractive: false,
|
||||
@@ -29,7 +36,7 @@ describe('SetupBuildCredentials', () => {
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore not to be created
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(0);
|
||||
expect(ctx.android.createKeystoreAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
it('sets up credentials when there are no prior credentials', async () => {
|
||||
const ctx = createCtxMock({
|
||||
@@ -44,7 +51,8 @@ describe('SetupBuildCredentials', () => {
|
||||
await setupBuildCredentialsAction.runAsync(ctx);
|
||||
|
||||
// expect keystore to be created
|
||||
expect(ctx.android.createKeystoreAsync as any).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.android.createKeystoreAsync).toHaveBeenCalled();
|
||||
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
|
||||
});
|
||||
it('errors in Non-Interactive Mode', async () => {
|
||||
const ctx = createCtxMock({
|
||||
|
||||
@@ -87,7 +87,7 @@ export default class IosCredentialsProvider {
|
||||
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
|
||||
if (isPushKeySetup) {
|
||||
Log.succeed(
|
||||
`Push Notifications setup for ${app.projectName}:${applicationTarget.bundleIdentifier}`
|
||||
`Push Notifications setup for ${app.projectName}: ${applicationTarget.bundleIdentifier}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const EnvironmentSecretMutation = {
|
||||
): Promise<EnvironmentSecretFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<CreateEnvironmentSecretForAppMutation>(
|
||||
.mutation<CreateEnvironmentSecretForAppMutation>(
|
||||
gql`
|
||||
mutation CreateEnvironmentSecretForApp(
|
||||
$input: CreateEnvironmentSecretInput!
|
||||
@@ -73,7 +73,7 @@ export const EnvironmentSecretMutation = {
|
||||
async delete(id: string): Promise<{ id: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<DeleteEnvironmentSecretMutation>(
|
||||
.mutation<DeleteEnvironmentSecretMutation>(
|
||||
gql`
|
||||
mutation DeleteEnvironmentSecret($id: String!) {
|
||||
environmentSecret {
|
||||
|
||||
@@ -19,7 +19,7 @@ export type EnvironmentSecretWithScope = EnvironmentSecretFragment & {
|
||||
};
|
||||
|
||||
export const EnvironmentSecretsQuery = {
|
||||
async byAcccountNameAsync(accountName: string): Promise<EnvironmentSecretFragment[]> {
|
||||
async byAccountNameAsync(accountName: string): Promise<EnvironmentSecretFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<EnvironmentSecretsByAccountNameQuery>(
|
||||
@@ -74,7 +74,7 @@ export const EnvironmentSecretsQuery = {
|
||||
projectFullName: string
|
||||
): Promise<EnvironmentSecretWithScope[]> {
|
||||
const [accountSecrets, appSecrets] = await Promise.all([
|
||||
this.byAcccountNameAsync(projectAccountName),
|
||||
this.byAccountNameAsync(projectAccountName),
|
||||
this.byAppIdAsync(projectFullName),
|
||||
]);
|
||||
|
||||
|
||||
@@ -11,3 +11,11 @@ export function toAppPlatform(platform: Platform): AppPlatform {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function toPlatform(appPlatform: AppPlatform): Platform {
|
||||
if (appPlatform === AppPlatform.Android) {
|
||||
return Platform.ANDROID;
|
||||
} else {
|
||||
return Platform.IOS;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { AppPlatform } from './graphql/generated';
|
||||
import { promptAsync } from './prompts';
|
||||
|
||||
export const appPlatformDisplayNames: Record<AppPlatform, string> = {
|
||||
[AppPlatform.Android]: 'Android',
|
||||
@@ -9,3 +12,47 @@ export const appPlatformEmojis = {
|
||||
[AppPlatform.Ios]: '🍎',
|
||||
[AppPlatform.Android]: '🤖',
|
||||
};
|
||||
|
||||
// for `eas build` and `eas submit`
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
Ios = 'ios',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
export const requestedPlatformDisplayNames: Record<RequestedPlatform, string> = {
|
||||
[RequestedPlatform.Android]: 'Android',
|
||||
[RequestedPlatform.Ios]: 'iOS',
|
||||
[RequestedPlatform.All]: 'Android and iOS',
|
||||
};
|
||||
|
||||
export async function selectRequestedPlatformAsync(platform?: string): Promise<RequestedPlatform> {
|
||||
if (
|
||||
platform &&
|
||||
Object.values(RequestedPlatform).includes(platform.toLowerCase() as RequestedPlatform)
|
||||
) {
|
||||
return platform.toLowerCase() as RequestedPlatform;
|
||||
}
|
||||
|
||||
const { requestedPlatform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Select platform',
|
||||
name: 'requestedPlatform',
|
||||
choices: [
|
||||
{ title: 'All', value: RequestedPlatform.All },
|
||||
{ title: 'Android', value: RequestedPlatform.Android },
|
||||
{ title: 'iOS', value: RequestedPlatform.Ios },
|
||||
],
|
||||
});
|
||||
return requestedPlatform;
|
||||
}
|
||||
|
||||
export function toPlatforms(requestedPlatform: RequestedPlatform): Platform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [Platform.ANDROID, Platform.IOS];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [Platform.ANDROID];
|
||||
} else {
|
||||
return [Platform.IOS];
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,24 @@ interface ProjectInfo {
|
||||
privacy?: AppPrivacy;
|
||||
}
|
||||
|
||||
const projectCache: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Ensures project exists on EAS servers. Registers it when it doesn't
|
||||
* @returns The project ID
|
||||
*/
|
||||
export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promise<string> {
|
||||
const { accountName, projectName } = projectInfo;
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
|
||||
if (projectFullName in projectCache) {
|
||||
return projectCache[projectFullName];
|
||||
}
|
||||
|
||||
const actor = await ensureLoggedInAsync();
|
||||
const account = findAccountByName(actor.accounts, accountName);
|
||||
assert(account, `You must have access to the ${accountName} account to run this command`);
|
||||
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
const projectLink = terminalLink(projectFullName, projectDashboardUrl);
|
||||
|
||||
@@ -36,6 +42,7 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
|
||||
const maybeId = await findProjectIdByAccountNameAndSlugNullableAsync(accountName, projectName);
|
||||
if (maybeId) {
|
||||
spinner.succeed(`Linked to project ${chalk.bold(projectLink)}`);
|
||||
projectCache[projectFullName] = maybeId;
|
||||
return maybeId;
|
||||
}
|
||||
|
||||
@@ -47,6 +54,7 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
|
||||
privacy: projectInfo.privacy,
|
||||
});
|
||||
spinner.succeed(`Created ${chalk.bold(projectLink)} on Expo`);
|
||||
projectCache[projectFullName] = id;
|
||||
return id;
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
@@ -84,8 +92,8 @@ async function findProjectIdByAccountNameAndSlugAsync(
|
||||
accountName: string,
|
||||
slug: string
|
||||
): Promise<string> {
|
||||
const project = await ProjectQuery.byUsernameAndSlugAsync(accountName, slug);
|
||||
return project.id;
|
||||
const { id } = await ProjectQuery.byUsernameAndSlugAsync(accountName, slug);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { exit } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import type MetroConfig from 'metro-config';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -38,7 +37,7 @@ export async function validateMetroConfigForManagedWorkflowAsync(ctx: BuildConte
|
||||
});
|
||||
if (shouldAbort) {
|
||||
Log.log('Aborting...');
|
||||
exit();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ interface ArchivePromptSource extends ArchiveSourceBase {
|
||||
export interface Archive {
|
||||
build?: BuildFragment;
|
||||
source: ArchiveSource;
|
||||
url: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export type ArchiveSource =
|
||||
@@ -62,8 +62,9 @@ export type ArchiveSource =
|
||||
|
||||
export async function getArchiveAsync(source: ArchiveSource): Promise<Archive> {
|
||||
switch (source.sourceType) {
|
||||
case ArchiveSourceType.prompt:
|
||||
case ArchiveSourceType.prompt: {
|
||||
return await handlePromptSourceAsync(source);
|
||||
}
|
||||
case ArchiveSourceType.url: {
|
||||
return await handleUrlSourceAsync(source);
|
||||
}
|
||||
@@ -115,7 +116,6 @@ async function handleLatestSourceAsync(source: ArchiveLatestSource): Promise<Arc
|
||||
|
||||
return {
|
||||
build: latestBuild,
|
||||
url: latestBuild.artifacts.buildUrl,
|
||||
source,
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -147,7 +147,6 @@ async function handleBuildIdSourceAsync(source: ArchiveBuildIdSource): Promise<A
|
||||
return {
|
||||
build,
|
||||
source,
|
||||
url: build.artifacts.buildUrl,
|
||||
};
|
||||
} catch (err) {
|
||||
Log.error(chalk.bold(`Couldn't find build for id ${source.id}`));
|
||||
@@ -166,7 +165,7 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise<Arc
|
||||
message: 'What would you like to submit?',
|
||||
choices: [
|
||||
{
|
||||
title: 'Latest build from EAS',
|
||||
title: 'Latest finished build from EAS',
|
||||
value: ArchiveSourceType.latest,
|
||||
},
|
||||
{ title: 'I have a url to the app archive', value: ArchiveSourceType.url },
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
IosSubmissionConfigInput,
|
||||
SubmissionFragment,
|
||||
} from '../graphql/generated';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { SubmissionContext } from './types';
|
||||
import { appPlatformDisplayNames } from '../platform';
|
||||
import { SubmissionContext } from './context';
|
||||
|
||||
export interface SubmissionInput<P extends Platform> {
|
||||
projectId: string;
|
||||
@@ -25,13 +27,14 @@ export default abstract class BaseSubmitter<P extends Platform, SubmissionOption
|
||||
submissionInput: SubmissionInput<P>
|
||||
): Promise<SubmissionFragment> {
|
||||
Log.addNewLineIfNone();
|
||||
const scheduleSpinner = ora('Scheduling submission').start();
|
||||
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(this.ctx.platform)];
|
||||
const scheduleSpinner = ora(`Scheduling ${platformDisplayName} submission`).start();
|
||||
try {
|
||||
const submission = this.createPlatformSubmissionAsync(submissionInput);
|
||||
scheduleSpinner.succeed();
|
||||
scheduleSpinner.succeed(`Scheduled ${platformDisplayName} submission`);
|
||||
return submission;
|
||||
} catch (err) {
|
||||
scheduleSpinner.fail('Failed to schedule submission');
|
||||
scheduleSpinner.fail(`Failed to schedule ${platformDisplayName} submission`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,5 +185,7 @@ function assertArchiveResult(
|
||||
expectedUrl: string = ARCHIVE_URL
|
||||
) {
|
||||
expect(archive.source.sourceType).toBe(expectedSourceType);
|
||||
expect(archive.url).toBe(expectedUrl);
|
||||
if (archive.url) {
|
||||
expect(archive.url).toBe(expectedUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack, AndroidSubmitProfile } from '@expo/eas-json';
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
|
||||
import { Result, result } from '@expo/results';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
|
||||
@@ -13,32 +13,12 @@ import Log from '../../log';
|
||||
import { getApplicationIdAsync } from '../../project/android/applicationId';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext, SubmitArchiveFlags } from '../types';
|
||||
import { SubmissionContext } from '../context';
|
||||
import { AndroidPackageSource, AndroidPackageSourceType } from './AndroidPackageSource';
|
||||
import AndroidSubmitter, { AndroidSubmissionOptions } from './AndroidSubmitter';
|
||||
import { ServiceAccountSource, ServiceAccountSourceType } from './ServiceAccountSource';
|
||||
|
||||
export default class AndroidSubmitCommand {
|
||||
static createContext({
|
||||
archiveFlags,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
}: {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
profile: AndroidSubmitProfile;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
}): SubmissionContext<Platform.ANDROID> {
|
||||
return {
|
||||
archiveFlags,
|
||||
platform: Platform.ANDROID,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private ctx: SubmissionContext<Platform.ANDROID>) {}
|
||||
|
||||
async runAsync(): Promise<SubmissionFragment> {
|
||||
@@ -65,7 +45,7 @@ export default class AndroidSubmitCommand {
|
||||
if (errored.length > 0) {
|
||||
const message = errored.map(err => err.reason?.message).join('\n');
|
||||
Log.error(message);
|
||||
throw new Error('Failed to submit the app');
|
||||
throw new Error('Submission failed');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -95,6 +75,8 @@ export default class AndroidSubmitCommand {
|
||||
sourceType: AndroidPackageSourceType.userDefined,
|
||||
androidPackage,
|
||||
});
|
||||
} else if (this.ctx.nonInteractive) {
|
||||
return result(new Error("Couldn't resolve the Android package."));
|
||||
} else {
|
||||
return result({
|
||||
sourceType: AndroidPackageSourceType.prompt,
|
||||
@@ -147,7 +129,11 @@ export default class AndroidSubmitCommand {
|
||||
}
|
||||
|
||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.ANDROID));
|
||||
try {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.ANDROID));
|
||||
} catch (err: any) {
|
||||
return result(err);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveServiceAccountSource(): Result<ServiceAccountSource> {
|
||||
@@ -157,6 +143,8 @@ export default class AndroidSubmitCommand {
|
||||
sourceType: ServiceAccountSourceType.path,
|
||||
path: serviceAccountKeyPath,
|
||||
});
|
||||
} else if (this.ctx.nonInteractive) {
|
||||
return result(new Error('Set serviceAccountKeyPath in the submit profile (eas.json).'));
|
||||
} else {
|
||||
return result({
|
||||
sourceType: ServiceAccountSourceType.detect,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
|
||||
import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import { createSubmissionContext } from '../../context';
|
||||
import { getLatestBuildForSubmissionAsync } from '../../utils/builds';
|
||||
import AndroidSubmitCommand from '../AndroidSubmitCommand';
|
||||
|
||||
@@ -71,11 +73,35 @@ describe(AndroidSubmitCommand, () => {
|
||||
asMock(getProjectIdAsync).mockClear();
|
||||
});
|
||||
|
||||
describe('non-interactive mode', () => {
|
||||
it("throws error if didn't provide serviceAccountKeyPath in the submit profile", async () => {
|
||||
const projectId = uuidv4();
|
||||
|
||||
const ctx = createSubmissionContext({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.apk',
|
||||
},
|
||||
profile: {
|
||||
track: AndroidReleaseTrack.internal,
|
||||
releaseStatus: AndroidReleaseStatus.draft,
|
||||
changesNotSentForReview: false,
|
||||
},
|
||||
nonInteractive: true,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await expect(command.runAsync()).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sending submission', () => {
|
||||
it('sends a request to Submission Service', async () => {
|
||||
const projectId = uuidv4();
|
||||
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
const ctx = createSubmissionContext({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
@@ -87,6 +113,7 @@ describe(AndroidSubmitCommand, () => {
|
||||
releaseStatus: AndroidReleaseStatus.draft,
|
||||
changesNotSentForReview: false,
|
||||
},
|
||||
nonInteractive: false,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
@@ -108,7 +135,8 @@ describe(AndroidSubmitCommand, () => {
|
||||
const projectId = uuidv4();
|
||||
asMock(getLatestBuildForSubmissionAsync).mockResolvedValueOnce(fakeBuildFragment);
|
||||
|
||||
const ctx = AndroidSubmitCommand.createContext({
|
||||
const ctx = createSubmissionContext({
|
||||
platform: Platform.ANDROID,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
@@ -120,6 +148,7 @@ describe(AndroidSubmitCommand, () => {
|
||||
releaseStatus: AndroidReleaseStatus.draft,
|
||||
changesNotSentForReview: false,
|
||||
},
|
||||
nonInteractive: false,
|
||||
});
|
||||
const command = new AndroidSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
@@ -127,7 +156,6 @@ describe(AndroidSubmitCommand, () => {
|
||||
expect(SubmissionMutation.createAndroidSubmissionAsync).toHaveBeenCalledWith({
|
||||
appId: projectId,
|
||||
config: {
|
||||
archiveUrl: 'http://expo.dev/fake.apk',
|
||||
applicationIdentifier: testProject.appJSON.expo.android?.package,
|
||||
googleServiceAccountKeyJson: fakeFiles['/google-service-account.json'],
|
||||
releaseStatus: SubmissionAndroidReleaseStatus.Draft,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
import { ArchiveSource, ArchiveSourceType } from './ArchiveSource';
|
||||
import { SubmissionContext } from './types';
|
||||
import { SubmissionContext } from './context';
|
||||
|
||||
export function resolveArchiveSource<T extends Platform>(
|
||||
ctx: SubmissionContext<T>,
|
||||
@@ -44,6 +44,8 @@ export function resolveArchiveSource<T extends Platform>(
|
||||
platform,
|
||||
projectId: ctx.projectId,
|
||||
};
|
||||
} else if (ctx.nonInteractive) {
|
||||
throw new Error('You need to specify the archive source when running in non-interactive mode ');
|
||||
} else {
|
||||
return {
|
||||
sourceType: ArchiveSourceType.prompt,
|
||||
|
||||
+12
@@ -7,6 +7,7 @@ export interface SubmissionContext<T extends Platform> {
|
||||
profile: SubmitProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
export interface SubmitArchiveFlags {
|
||||
@@ -15,3 +16,14 @@ export interface SubmitArchiveFlags {
|
||||
path?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export function createSubmissionContext<T extends Platform>(params: {
|
||||
platform: T;
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
profile: SubmitProfile<T>;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
nonInteractive: boolean;
|
||||
}): SubmissionContext<T> {
|
||||
return params;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import Log from '../../log';
|
||||
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { SubmissionContext } from '../types';
|
||||
import { SubmissionContext } from '../context';
|
||||
import { sanitizeLanguage } from './utils/language';
|
||||
|
||||
interface CreateAppOptions {
|
||||
@@ -32,11 +32,8 @@ type AppStoreResult = {
|
||||
export async function ensureAppStoreConnectAppExistsAsync(
|
||||
ctx: SubmissionContext<Platform.IOS>
|
||||
): Promise<AppStoreResult> {
|
||||
const projectConfig = getConfig(ctx.projectDir, { skipSDKVersionRequirement: true });
|
||||
const { exp } = projectConfig;
|
||||
|
||||
const { exp } = getConfig(ctx.projectDir, { skipSDKVersionRequirement: true });
|
||||
const { appName, language } = ctx.profile;
|
||||
|
||||
const options = {
|
||||
...ctx.profile,
|
||||
bundleIdentifier: await getBundleIdentifierAsync(ctx.projectDir, exp),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { IosSubmitProfile } from '@expo/eas-json';
|
||||
import { Result, result } from '@expo/results';
|
||||
import chalk from 'chalk';
|
||||
import getenv from 'getenv';
|
||||
@@ -11,7 +10,7 @@ import { promptAsync } from '../../prompts';
|
||||
import UserSettings from '../../user/UserSettings';
|
||||
import { ArchiveSource } from '../ArchiveSource';
|
||||
import { resolveArchiveSource } from '../commons';
|
||||
import { SubmissionContext, SubmitArchiveFlags } from '../types';
|
||||
import { SubmissionContext } from '../context';
|
||||
import { ensureAppStoreConnectAppExistsAsync } from './AppProduce';
|
||||
import {
|
||||
AppSpecificPasswordSource,
|
||||
@@ -20,26 +19,6 @@ import {
|
||||
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
|
||||
|
||||
export default class IosSubmitCommand {
|
||||
static createContext({
|
||||
archiveFlags,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
}: {
|
||||
archiveFlags: SubmitArchiveFlags;
|
||||
profile: IosSubmitProfile;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
}): SubmissionContext<Platform.IOS> {
|
||||
return {
|
||||
archiveFlags,
|
||||
platform: Platform.IOS,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private ctx: SubmissionContext<Platform.IOS>) {}
|
||||
|
||||
async runAsync(): Promise<SubmissionFragment> {
|
||||
@@ -52,20 +31,25 @@ export default class IosSubmitCommand {
|
||||
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
|
||||
const archiveSource = this.resolveArchiveSource();
|
||||
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
||||
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
|
||||
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
|
||||
|
||||
const errored = [archiveSource, appSpecificPasswordSource].filter(r => !r.ok);
|
||||
const errored = [
|
||||
archiveSource,
|
||||
appSpecificPasswordSource,
|
||||
ascAppIdentifier,
|
||||
appleIdUsername,
|
||||
].filter(r => !r.ok);
|
||||
if (errored.length > 0) {
|
||||
const message = errored.map(err => err.reason?.message).join('\n');
|
||||
Log.error(message);
|
||||
throw new Error('Failed to submit the app');
|
||||
throw new Error('Submission failed');
|
||||
}
|
||||
|
||||
const { appleIdUsername, ascAppIdentifier } = await this.getAppStoreInfoAsync();
|
||||
|
||||
return {
|
||||
projectId: this.ctx.projectId,
|
||||
appleIdUsername,
|
||||
ascAppIdentifier,
|
||||
appleIdUsername: appleIdUsername.enforceValue(),
|
||||
ascAppIdentifier: ascAppIdentifier.enforceValue(),
|
||||
archiveSource: archiveSource.enforceValue(),
|
||||
appSpecificPasswordSource: appSpecificPasswordSource.enforceValue(),
|
||||
};
|
||||
@@ -79,77 +63,79 @@ export default class IosSubmitCommand {
|
||||
sourceType: AppSpecificPasswordSourceType.userDefined,
|
||||
appSpecificPassword: envAppSpecificPassword,
|
||||
});
|
||||
} else if (this.ctx.nonInteractive) {
|
||||
return result(new Error('Set the EXPO_APPLE_APP_SPECIFIC_PASSWORD environment variable.'));
|
||||
} else {
|
||||
return result({
|
||||
sourceType: AppSpecificPasswordSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
return result({
|
||||
sourceType: AppSpecificPasswordSourceType.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.IOS));
|
||||
try {
|
||||
return result(resolveArchiveSource(this.ctx, Platform.IOS));
|
||||
} catch (err: any) {
|
||||
return result(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns App Store related information required for build submission
|
||||
* It is:
|
||||
* - User Apple ID
|
||||
* - App Store Connect app ID (appAppleId)
|
||||
*/
|
||||
private async getAppStoreInfoAsync(): Promise<{
|
||||
appleIdUsername: string;
|
||||
ascAppIdentifier: string;
|
||||
}> {
|
||||
private async resolveAscAppIdentifierAsync(): Promise<Result<string>> {
|
||||
const { ascAppId } = this.ctx.profile;
|
||||
|
||||
if (ascAppId) {
|
||||
return {
|
||||
appleIdUsername: await this.getAppleIdAsync(),
|
||||
ascAppIdentifier: ascAppId,
|
||||
};
|
||||
return result(ascAppId);
|
||||
} else if (this.ctx.nonInteractive) {
|
||||
return result(new Error('Set ascAppId in the submit profile (eas.json).'));
|
||||
} else {
|
||||
Log.log(
|
||||
wrapAnsi(
|
||||
chalk.italic(
|
||||
`Ensuring your app exists on App Store Connect. This step can be skipped by providing ascAppId in the submit profile. ${learnMore(
|
||||
'https://expo.fyi/asc-app-id'
|
||||
)}`
|
||||
),
|
||||
process.stdout.columns || 80
|
||||
)
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
try {
|
||||
const { ascAppIdentifier } = await ensureAppStoreConnectAppExistsAsync(this.ctx);
|
||||
return result(ascAppIdentifier);
|
||||
} catch (err: any) {
|
||||
return result(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveAppleIdUsernameAsync(): Promise<Result<string>> {
|
||||
if (this.ctx.profile.appleId) {
|
||||
return result(this.ctx.profile.appleId);
|
||||
}
|
||||
|
||||
Log.log(
|
||||
wrapAnsi(
|
||||
chalk.italic(
|
||||
`Ensuring your app exists on App Store Connect. This step can be skipped by providing ascAppId in the submit profile. ${learnMore(
|
||||
'https://expo.fyi/asc-app-id'
|
||||
)}`
|
||||
),
|
||||
process.stdout.columns || 80
|
||||
)
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
return await ensureAppStoreConnectAppExistsAsync(this.ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is going to be used only when `produce` is not being run,
|
||||
* and we don't need to call full credentials.authenticateAsync()
|
||||
* and we just need apple ID
|
||||
*/
|
||||
private async getAppleIdAsync(): Promise<string> {
|
||||
const { appleId } = this.ctx.profile;
|
||||
const envAppleId = getenv.string('EXPO_APPLE_ID', '');
|
||||
|
||||
if (appleId) {
|
||||
return appleId;
|
||||
} else if (envAppleId) {
|
||||
return envAppleId;
|
||||
if (envAppleId) {
|
||||
return result(envAppleId);
|
||||
}
|
||||
|
||||
// Get the email address that was last used and set it as
|
||||
// the default value for quicker authentication.
|
||||
const lastAppleId = await UserSettings.getAsync('appleId', null);
|
||||
|
||||
const { appleId: promptAppleId } = await promptAsync({
|
||||
if (this.ctx.nonInteractive) {
|
||||
if (lastAppleId) {
|
||||
return result(lastAppleId);
|
||||
} else {
|
||||
return result(new Error('Set appleId in the submit profile (eas.json).'));
|
||||
}
|
||||
}
|
||||
|
||||
const { appleId } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'appleId',
|
||||
message: `Enter your Apple ID:`,
|
||||
validate: (val: string) => !!val,
|
||||
initial: lastAppleId ?? undefined,
|
||||
});
|
||||
|
||||
return promptAppleId;
|
||||
return result(appleId);
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,6 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
|
||||
const appSpecificPassword = await getAppSpecificPasswordAsync(
|
||||
this.options.appSpecificPasswordSource
|
||||
);
|
||||
|
||||
return {
|
||||
archive,
|
||||
appSpecificPassword,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { vol } from 'memfs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -6,6 +7,7 @@ import { jester as mockJester } from '../../../credentials/__tests__/fixtures-co
|
||||
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
|
||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||
import { getProjectIdAsync } from '../../../project/projectUtils';
|
||||
import { createSubmissionContext } from '../../context';
|
||||
import IosSubmitCommand from '../IosSubmitCommand';
|
||||
|
||||
jest.mock('fs');
|
||||
@@ -51,13 +53,35 @@ describe(IosSubmitCommand, () => {
|
||||
asMock(getProjectIdAsync).mockClear();
|
||||
});
|
||||
|
||||
describe('non-interactive mode', () => {
|
||||
it("throws error if didn't provide appleId and ascAppId in the submit profile", async () => {
|
||||
const projectId = uuidv4();
|
||||
|
||||
const ctx = createSubmissionContext({
|
||||
platform: Platform.IOS,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
url: 'http://expo.dev/fake.ipa',
|
||||
},
|
||||
profile: {
|
||||
language: 'en-US',
|
||||
},
|
||||
nonInteractive: true,
|
||||
});
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
await expect(command.runAsync()).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sending submission', () => {
|
||||
it('sends a request to Submission Service', async () => {
|
||||
const projectId = uuidv4();
|
||||
|
||||
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
|
||||
|
||||
const ctx = IosSubmitCommand.createContext({
|
||||
const ctx = createSubmissionContext({
|
||||
platform: Platform.IOS,
|
||||
projectDir: testProject.projectRoot,
|
||||
projectId,
|
||||
archiveFlags: {
|
||||
@@ -68,6 +92,7 @@ describe(IosSubmitCommand, () => {
|
||||
appleId: '[email protected]',
|
||||
ascAppId: '12345678',
|
||||
},
|
||||
nonInteractive: false,
|
||||
});
|
||||
const command = new IosSubmitCommand(ctx);
|
||||
await command.runAsync();
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AppPlatform, SubmissionFragment, SubmissionStatus } from '../graphql/generated';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../platform';
|
||||
import AndroidSubmitCommand from './android/AndroidSubmitCommand';
|
||||
import { SubmissionContext } from './context';
|
||||
import IosSubmitCommand from './ios/IosSubmitCommand';
|
||||
import { displayLogsAsync } from './utils/logs';
|
||||
import { waitForSubmissionsEndAsync } from './utils/wait';
|
||||
|
||||
export async function submitAsync<T extends Platform>(
|
||||
ctx: SubmissionContext<T>
|
||||
): Promise<SubmissionFragment> {
|
||||
const command =
|
||||
ctx.platform === Platform.ANDROID
|
||||
? new AndroidSubmitCommand(ctx as SubmissionContext<Platform.ANDROID>)
|
||||
: new IosSubmitCommand(ctx as SubmissionContext<Platform.IOS>);
|
||||
return command.runAsync();
|
||||
}
|
||||
|
||||
export async function waitToCompleteAsync(
|
||||
submissions: SubmissionFragment[],
|
||||
{ verbose = false }: { verbose?: boolean } = {}
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
const completedSubmissions = await waitForSubmissionsEndAsync(submissions);
|
||||
const moreSubmissions = completedSubmissions.length > 1;
|
||||
if (moreSubmissions) {
|
||||
Log.newLine();
|
||||
}
|
||||
for (const submission of completedSubmissions) {
|
||||
if (moreSubmissions) {
|
||||
Log.log(
|
||||
`${appPlatformEmojis[submission.platform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[submission.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (submission.platform === AppPlatform.Android) {
|
||||
printInstructionsForAndroidSubmission(submission);
|
||||
} else {
|
||||
printInstructionsForIosSubmission(submission);
|
||||
}
|
||||
await displayLogsAsync(submission, { verbose, moreSubmissions });
|
||||
if (moreSubmissions) {
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||
}
|
||||
|
||||
function printInstructionsForAndroidSubmission(submission: SubmissionFragment): void {
|
||||
if (submission.status === SubmissionStatus.Finished) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('All done!');
|
||||
}
|
||||
}
|
||||
|
||||
function printInstructionsForIosSubmission(submission: SubmissionFragment): void {
|
||||
if (submission.status === SubmissionStatus.Finished) {
|
||||
const logMsg = [
|
||||
chalk.bold('Your binary has been successfully uploaded to App Store Connect!'),
|
||||
'- It is now being processed by Apple - you will receive an e-mail when the processing finishes.',
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.',
|
||||
// ascAppIdentifier should be always available for ios submissions but check it anyway
|
||||
submission.iosConfig?.ascAppIdentifier &&
|
||||
`- When it’s done, you can see your build here: ${learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${submission.iosConfig?.ascAppIdentifier}/appstore/ios`,
|
||||
{ learnMoreMessage: '' }
|
||||
)}`,
|
||||
].join('\n');
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(logMsg);
|
||||
}
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(submissions: SubmissionFragment[]): void {
|
||||
const nonFinishedSubmissions = submissions.filter(
|
||||
({ status }) => status !== SubmissionStatus.Finished
|
||||
);
|
||||
if (nonFinishedSubmissions.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,32 @@
|
||||
import { AppPlatform, BuildArtifacts, BuildFragment, BuildStatus } from '../../graphql/generated';
|
||||
import { AppPlatform, BuildFragment, BuildStatus } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
|
||||
// `BuildFragment` with non-null `artifacts.buildUrl`
|
||||
type BuildFragmentWithArtifact = BuildFragment & {
|
||||
artifacts: BuildArtifacts & { buildUrl: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets build by ID and ensures that `artifacts.buildUrl` exists
|
||||
*/
|
||||
export async function getBuildByIdForSubmissionAsync(
|
||||
platform: AppPlatform,
|
||||
buildId: string
|
||||
): Promise<BuildFragmentWithArtifact> {
|
||||
): Promise<BuildFragment> {
|
||||
const build = await BuildQuery.byIdAsync(buildId);
|
||||
|
||||
if (build.platform !== platform) {
|
||||
throw new Error("Build platform doesn't match!");
|
||||
}
|
||||
|
||||
if (!buildFragmentHasArtifact(build)) {
|
||||
throw new Error('Build has no artifacts or build URL is not defined.');
|
||||
}
|
||||
|
||||
return build;
|
||||
}
|
||||
|
||||
export async function getLatestBuildForSubmissionAsync(
|
||||
platform: AppPlatform,
|
||||
appId: string
|
||||
): Promise<BuildFragmentWithArtifact | null> {
|
||||
const builds = await BuildQuery.allForAppAsync(appId, {
|
||||
): Promise<BuildFragment | null> {
|
||||
const [build] = await BuildQuery.allForAppAsync(appId, {
|
||||
limit: 1,
|
||||
filter: {
|
||||
platform,
|
||||
status: BuildStatus.Finished,
|
||||
},
|
||||
});
|
||||
|
||||
if (builds.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const build = builds[0];
|
||||
if (!buildFragmentHasArtifact(build)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return build;
|
||||
}
|
||||
|
||||
// Utility function needed to make TypeScript happy
|
||||
function buildFragmentHasArtifact(build: BuildFragment): build is BuildFragmentWithArtifact {
|
||||
return build.artifacts?.buildUrl != null;
|
||||
}
|
||||
@@ -2,16 +2,19 @@ import chalk from 'chalk';
|
||||
import got from 'got';
|
||||
|
||||
import { SubmissionFragment, SubmissionStatus } from '../../graphql/generated';
|
||||
import { default as LogModule } from '../../log';
|
||||
import Log from '../../log';
|
||||
import { printSubmissionError } from './errors';
|
||||
|
||||
export async function displayLogsAsync(
|
||||
submission: SubmissionFragment,
|
||||
{ verbose = false }: { verbose?: boolean } = {}
|
||||
{ verbose = false, moreSubmissions = false } = {}
|
||||
): Promise<void> {
|
||||
let printedUnknownError = false;
|
||||
if (submission.status === SubmissionStatus.Errored && submission.error) {
|
||||
printedUnknownError = printSubmissionError(submission.error);
|
||||
} else if (submission.status === SubmissionStatus.Canceled && moreSubmissions) {
|
||||
Log.newLine();
|
||||
Log.error('Submission has been canceled');
|
||||
}
|
||||
if (printedUnknownError || verbose) {
|
||||
await downloadAndPrintSubmissionLogsAsync(submission);
|
||||
@@ -24,28 +27,28 @@ async function downloadAndPrintSubmissionLogsAsync(submission: SubmissionFragmen
|
||||
}
|
||||
const { body: data } = await got.get(submission.logsUrl);
|
||||
const logs = parseLogs(data);
|
||||
LogModule.addNewLineIfNone();
|
||||
Log.addNewLineIfNone();
|
||||
const prefix = chalk.blueBright('[logs] ');
|
||||
for (const { level, msg } of logs) {
|
||||
const msgWithPrefix = `${prefix}${msg}`;
|
||||
if (level === 'error') {
|
||||
LogModule.error(msgWithPrefix);
|
||||
Log.error(msgWithPrefix);
|
||||
} else if (level === 'warn') {
|
||||
LogModule.warn(msgWithPrefix);
|
||||
Log.warn(msgWithPrefix);
|
||||
} else {
|
||||
LogModule.log(msgWithPrefix);
|
||||
Log.log(msgWithPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Log {
|
||||
interface LogLine {
|
||||
level: 'error' | 'warn' | 'info';
|
||||
msg: string;
|
||||
}
|
||||
|
||||
function parseLogs(logs: string): Log[] {
|
||||
function parseLogs(logs: string): LogLine[] {
|
||||
const lines = logs.split('\n');
|
||||
const result: Log[] = [];
|
||||
const result: LogLine[] = [];
|
||||
for (const line of lines) {
|
||||
let parsedLine;
|
||||
try {
|
||||
@@ -53,7 +56,7 @@ function parseLogs(logs: string): Log[] {
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
let level: Log['level'];
|
||||
let level: LogLine['level'];
|
||||
const { level: levelNumber, msg } = parsedLine;
|
||||
if (levelNumber >= 50) {
|
||||
level = 'error';
|
||||
|
||||
@@ -68,7 +68,7 @@ export function printSummary<T>(summary: T, keyMap: Record<keyof T, string>): vo
|
||||
fields.push({ label, value });
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(formatFields(fields, { labelFormat: chalk.bold.cyan }));
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with the eas.json",
|
||||
"version": "0.26.0",
|
||||
"version": "0.27.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "0.2.46",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "10.0.0",
|
||||
"joi": "17.4.2",
|
||||
"tslib": "2.3.1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import envString from 'env-string';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
@@ -10,7 +11,11 @@ import {
|
||||
IosSubmitProfileSchema,
|
||||
MinimalEasJsonSchema,
|
||||
} from './EasJsonSchema';
|
||||
import { SubmitProfile } from './EasSubmit.types';
|
||||
import {
|
||||
AndroidSubmitProfileFieldsToEvaluate,
|
||||
IosSubmitProfileFieldsToEvaluate,
|
||||
SubmitProfile,
|
||||
} from './EasSubmit.types';
|
||||
|
||||
interface EasJsonPreValidation {
|
||||
build: { [profile: string]: object };
|
||||
@@ -93,7 +98,7 @@ export class EasJsonReader {
|
||||
if (!profile) {
|
||||
throw new Error(`There is no profile named ${profileName} in eas.json for ${platform}.`);
|
||||
}
|
||||
return profile as SubmitProfile<T>;
|
||||
return this.evaluateFields(platform, profile as SubmitProfile<T>);
|
||||
}
|
||||
|
||||
public async readAndValidateAsync(): Promise<EasJson> {
|
||||
@@ -151,6 +156,24 @@ export class EasJsonReader {
|
||||
throw new Error(`There is no profile named ${profileName} in eas.json.`);
|
||||
}
|
||||
}
|
||||
|
||||
private evaluateFields<T extends Platform>(
|
||||
platform: T,
|
||||
profile: SubmitProfile<T>
|
||||
): SubmitProfile<T> {
|
||||
const fields =
|
||||
platform === Platform.ANDROID
|
||||
? AndroidSubmitProfileFieldsToEvaluate
|
||||
: IosSubmitProfileFieldsToEvaluate;
|
||||
const evaluatedProfile = { ...profile };
|
||||
for (const field of fields) {
|
||||
if (field in evaluatedProfile) {
|
||||
// @ts-ignore
|
||||
evaluatedProfile[field] = envString(evaluatedProfile[field], process.env);
|
||||
}
|
||||
}
|
||||
return evaluatedProfile;
|
||||
}
|
||||
}
|
||||
|
||||
export function profileMerge(base: RawBuildProfile, update: RawBuildProfile): RawBuildProfile {
|
||||
|
||||
@@ -21,6 +21,10 @@ export interface AndroidSubmitProfile {
|
||||
changesNotSentForReview: boolean;
|
||||
}
|
||||
|
||||
export const AndroidSubmitProfileFieldsToEvaluate: (keyof AndroidSubmitProfile)[] = [
|
||||
'serviceAccountKeyPath',
|
||||
];
|
||||
|
||||
export interface IosSubmitProfile {
|
||||
appleId?: string;
|
||||
ascAppId?: string;
|
||||
@@ -31,6 +35,8 @@ export interface IosSubmitProfile {
|
||||
appName?: string;
|
||||
}
|
||||
|
||||
export const IosSubmitProfileFieldsToEvaluate: (keyof IosSubmitProfile)[] = [];
|
||||
|
||||
export type SubmitProfile<TPlatform extends Platform = Platform> =
|
||||
TPlatform extends Platform.ANDROID
|
||||
? AndroidSubmitProfile
|
||||
|
||||
@@ -59,6 +59,35 @@ test('android config with all required values', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('android config with serviceAccountKeyPath set to env var', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
submit: {
|
||||
release: {
|
||||
android: {
|
||||
serviceAccountKeyPath: '$GOOGLE_SERVICE_ACCOUNT',
|
||||
track: 'beta',
|
||||
releaseStatus: 'completed',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
|
||||
const reader = new EasJsonReader('/project');
|
||||
const androidProfile = await reader.readSubmitProfileAsync(Platform.ANDROID, 'release');
|
||||
|
||||
expect(androidProfile).toEqual({
|
||||
serviceAccountKeyPath: './path.json',
|
||||
track: 'beta',
|
||||
releaseStatus: 'completed',
|
||||
changesNotSentForReview: false,
|
||||
});
|
||||
} finally {
|
||||
process.env.GOOGLE_SERVICE_ACCOUNT = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
test('ios config with all required values', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
submit: {
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare module 'env-string' {
|
||||
export default function envString(str: string, env?: Record<string, string | undefined>): string;
|
||||
}
|
||||
@@ -6029,6 +6029,11 @@ [email protected], env-paths@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||
integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
|
||||
|
||||
[email protected]:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/env-string/-/env-string-1.0.1.tgz#92ddefeb651c0f2d15bf89dc13be7596733801a4"
|
||||
integrity sha512-/DhCJDf5DSFK32joQiWRpWrT0h7p3hVQfMKxiBb7Nt8C8IF8BYyPtclDnuGGLOoj16d/8udKeiE7JbkotDmorQ==
|
||||
|
||||
[email protected], envinfo@^7.7.4:
|
||||
version "7.8.1"
|
||||
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
|
||||
|
||||
Reference in new issue
Block a user