Compare commits

...
32 Commits
Author SHA1 Message Date
Dominik Sokal 5e5d3626f4 v0.4.2 2021-02-18 11:36:14 +01:00
Dominik Sokal 5808c5b412 add CHANGELOG.md (#239) 2021-02-18 11:30:13 +01:00
Dominik Sokal cd64392d44 [eas-cli] bump @expo/config-plugins to 1.0.19-alpha.0 (#238) 2021-02-18 11:20:39 +01:00
Wojciech Kozyra 79f889e16c v0.4.1 2021-02-16 19:17:33 +01:00
Wojciech Kozyra 595bfecf1c [eas-cli] fix "buildType" is not allowed error 2021-02-16 19:15:10 +01:00
Dominik Sokal ced2e192eb v0.4.0 2021-02-16 15:08:05 +01:00
Dominik Sokal ee91612c07 [eas-cli][eas-json] add schemeBuildConfiguration to eas.json (#234)
* [eas-cli] use newer @expo/config-plugins

* [eas-cli][eas-json] add buildConfiguration to eas.json

* address PR feedback

* upgrade @expo/eas-build-job and @expo/config-plugins

* [eas-json] reuse type from @expo/eas-build-job

* [eas-json] fix exports
2021-02-16 14:40:57 +01:00
Quinlan Jung 4d815c1fd9 [eas-cli] tests for credential removal (#236)
* [eas-cli] tests for credential removal

* fix tsc
2021-02-15 18:11:51 -08:00
Quinlan Jung d2687a55b7 [eas-cli] basic RemoveDistCert (and RemoveProvisioningProfile) (#221)
* [www] remove dist cert and prov profile

* pr feedback

* fix tests

* cleanup removal code

* fix tsc
2021-02-15 17:50:24 -08:00
jkh 41142d33dc [eas-cli] remove trailing whitespace from default branch name (#235) 2021-02-15 13:12:52 -08:00
jkh 52ab6107c5 [eas-cli] rename EAS release to branch (#233) 2021-02-15 12:41:12 -08:00
Wojciech Kozyra 56ac348ed4 [eas-cli] configurable builder environment (#230) 2021-02-15 08:55:07 +01:00
Dominik Sokal 1ac0af53ab [ENG-9] [eas-cli] implement automatic version bumping for ios builds (#231)
* [eas-cli] implement automatic version bumping for ios builds

* [eas-cli] autoincrement -> autoIncrement

* [eas-cli] code style fix

* [eas-cli][eas-json] address PR feedback

* more pr feedback
2021-02-12 14:37:19 +01:00
Wojciech Kozyra f9361b471c [eas-cli] fix running builds from subdirectory (#229) 2021-02-09 11:08:38 +01:00
Paul Ridgway 8f39e0dcde Allow wait to be set to false for eas build #225 (#226) 2021-02-08 13:23:31 +01:00
Wojciech Kozyra 81de402d2d [eas-cli] add build:cancel command (#219)
* [eas-cli] add build:cancel command

* review feedback
2021-02-08 13:23:08 +01:00
Wojciech Kozyra 9d6e3f52d8 [eas-cli] Fix missing android.package error (#224) 2021-02-08 13:20:22 +01:00
Will Schurman 6a0a9bbaaa Fix potential broken this binding when passing logger function as argument (#227) 2021-02-04 09:38:02 -08:00
Will Schurman 24bc29b3cd Refactor log module so that weird __esModule mocks are not needed (#223) 2021-02-03 16:26:58 -08:00
Brent Vatne aac5ebff73 v0.3.1 2021-01-25 12:05:30 -08:00
Wojciech KozyraandDominik Sokal 61cc5b0a4a [eas-cli] support canceled build status (#217)
* [eas-cli] support `canceled` build status

* Update packages/eas-cli/src/build/create.ts

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-01-22 11:00:56 +01:00
Wojciech Kozyra 1383783c33 [eas-cli] check if tty is atched on prompt (#216) 2021-01-21 15:24:25 +01:00
Dominik Sokal 971129be6b [eas-cli] improve monorepo support - fix configuring expo-updates (#214)
* [eas-cli] improve monorepo support - fix configuring expo-updates

* [eas-cli] bump @expo/config-plugins to 1.0.15-alpha.0
2021-01-21 12:08:30 +01:00
Quinlan Jung f4fb10483a [eas-cli] filter stale profiles (#215)
* [eas-cli] filter stale profiles

* pr feedback
2021-01-20 15:30:54 -08:00
Wojciech Kozyra 256a0e1f22 [eas-cli] return non-zero exit code if prompt is displayed on CI (#208) 2021-01-20 14:57:08 +01:00
William Schurman b82f0b04fe v0.3.0 2021-01-19 11:57:24 -08:00
Quinlan Jung 9714420f90 [eas-cli] select dist certs (#205)
* [eas-cli] select dist certs

* remove validity unknown

* pr feedback

* tests
2021-01-19 11:50:02 -08:00
Dominik Sokal 0054f0bc26 update release instructions (#212) 2021-01-19 20:42:54 +01:00
Will Schurman 38f565d849 [eas-cli] 2FA support (#204) 2021-01-19 09:37:45 -08:00
Dominik Sokal 1aff7ae2a3 fix typo 2021-01-19 16:22:20 +01:00
Dominik Sokal 31020047ca v0.2.1 2021-01-19 11:52:08 +01:00
Dominik Sokal 26778ec52b [eas-cli] hide release:publish command (#211) 2021-01-19 11:49:19 +01:00
149 changed files with 3872 additions and 1407 deletions

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
# Changelog
This is the log of notable changes to EAS CLI and related packages.
## main
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
- Fix detecting application target (iOS builds). ([#238](https://github.com/expo/eas-cli/pull/2238) by [@dsokal](https://github.com/dsokal))
## [0.4.1](https://github.com/expo/eas-cli/releases/tag/v0.4.1) - 2021-02-16
### 🐛 Bug fixes
- Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
### 🎉 New features
- Add build:cancel command. ([#219](https://github.com/expo/eas-cli/pull/219) by [@wkozyra95](https://github.com/wkozyra95))
- Implement version auto increment for iOS builds. ([#231](https://github.com/expo/eas-cli/pull/231) by [@dsokal](https://github.com/dsokal))
- Add support for builder environment customizations. ([#230](https://github.com/expo/eas-cli/pull/230) by [@wkozyra95](https://github.com/wkozyra95))
- Add `schemeBuildConfiguration` option for generic iOS builds. ([#234](https://github.com/expo/eas-cli/pull/234) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix `--no-wait` flag for `eas build`. ([#226](https://github.com/expo/eas-cli/pull/226) by [paul-ridgway](https://github.com/paul-ridgway))
- Fix running builds from project subdirectories. ([#229](https://github.com/expo/eas-cli/pull/229) by [@wkozyra95](https://github.com/wkozyra95))
+1
View File
@@ -6,3 +6,4 @@
1. Wait until the `release` GitHub workflow has finished.
1. Once the release ready to go, you can go to
https://github.com/expo/eas-cli/releases and mark it as not draft.
1. Run `yarn lerna publish from-git` in the repository root folder. This will publish the packages to npm.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.2.0"
"version": "0.4.2"
}
+30 -36
View File
@@ -21,7 +21,7 @@ $ npm install -g eas-cli
$ eas COMMAND
running command...
$ eas (-v|--version|version)
eas-cli/0.2.0 darwin-x64 node-v12.18.4
eas-cli/0.4.2 darwin-x64 node-v14.15.4
$ eas --help [COMMAND]
USAGE
$ eas COMMAND
@@ -36,15 +36,15 @@ USAGE
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:list`](#eas-buildlist)
* [`eas build:view [BUILDID]`](#eas-buildview-buildid)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:list`](#eas-devicelist)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas release:publish`](#eas-releasepublish)
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
## `eas account:login`
@@ -59,7 +59,7 @@ ALIASES
$ eas login
```
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/account/login.ts)_
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/account/login.ts)_
## `eas account:logout`
@@ -73,7 +73,7 @@ ALIASES
$ eas logout
```
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/account/logout.ts)_
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/account/logout.ts)_
## `eas account:view`
@@ -87,7 +87,7 @@ ALIASES
$ eas whoami
```
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/account/view.ts)_
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/account/view.ts)_
## `eas build`
@@ -103,10 +103,21 @@ OPTIONS
--profile=profile [default: release] Name of the build profile from eas.json
--skip-credentials-check Skip validation of build credentials
--skip-project-configuration Skip project configuration
--wait Wait for build(s) to complete
--[no-]wait Wait for build(s) to complete
```
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/build/index.ts)_
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
Cancel a build.
```
USAGE
$ eas build:cancel [BUILD_ID]
```
_See code: [build/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/cancel.ts)_
## `eas build:configure`
@@ -121,7 +132,7 @@ OPTIONS
--allow-experimental Enable experimental configuration steps.
```
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/build/configure.ts)_
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/configure.ts)_
## `eas build:list`
@@ -134,21 +145,21 @@ USAGE
OPTIONS
--limit=limit
--platform=(all|android|ios)
--status=(in-queue|in-progress|errored|finished)
--status=(in-queue|in-progress|errored|finished|canceled)
```
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/build/list.ts)_
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/list.ts)_
## `eas build:view [BUILDID]`
## `eas build:view [BUILD_ID]`
view a build for your project
```
USAGE
$ eas build:view [BUILDID]
$ eas build:view [BUILD_ID]
```
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/build/view.ts)_
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/build/view.ts)_
## `eas credentials`
@@ -159,7 +170,7 @@ USAGE
$ eas credentials
```
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/credentials.ts)_
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/credentials.ts)_
## `eas device:create`
@@ -170,7 +181,7 @@ USAGE
$ eas device:create
```
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/device/create.ts)_
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/device/create.ts)_
## `eas device:list`
@@ -184,7 +195,7 @@ OPTIONS
--apple-team-id=apple-team-id
```
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/device/list.ts)_
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -195,7 +206,7 @@ USAGE
$ eas device:view [UDID]
```
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/device/view.ts)_
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/device/view.ts)_
## `eas help [COMMAND]`
@@ -214,23 +225,6 @@ OPTIONS
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
## `eas release:publish`
Publish an update group to a release.
```
USAGE
$ eas release:publish
OPTIONS
--input-dir=input-dir [default: dist] location of the bundle
--json return a json with the edited release's ID and name.
--message=message Short message describing the updates.
--release=release current name of the release.
```
_See code: [build/commands/release/publish.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/release/publish.ts)_
## `eas submit --platform=(android|ios)`
Submits build artifact to app store
@@ -309,5 +303,5 @@ EXAMPLES
and provide its App ID
```
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.2.0/build/commands/submit.ts)_
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.4.2/build/commands/submit.ts)_
<!-- commandsstop -->
@@ -1,3 +0,0 @@
const getConfig = jest.fn(() => ({ exp: {} }));
export { getConfig };
File diff suppressed because it is too large. Load diff
+7 -4
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.2.0",
"version": "0.4.2",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -10,9 +10,9 @@
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.17",
"@expo/config": "~3.3.19",
"@expo/config-plugins": "^1.0.14",
"@expo/eas-build-job": "^0.2.0",
"@expo/eas-json": "^0.2.0",
"@expo/config-plugins": "1.0.19-alpha.0",
"@expo/eas-build-job": "0.2.11",
"@expo/eas-json": "^0.4.1",
"@expo/json-file": "^8.2.24",
"@expo/plist": "^0.0.11",
"@expo/plugin-warn-if-update-available": "^1.7.0",
@@ -50,6 +50,7 @@
"progress": "^2.0.3",
"prompts": "^2.3.2",
"qrcode-terminal": "^0.12.0",
"semver": "^7.3.4",
"strip-ansi": "^6.0.0",
"tar": "^6.0.5",
"tempy": "^0.7.0",
@@ -82,6 +83,7 @@
"@types/node-forge": "^0.9.5",
"@types/progress": "^2.0.3",
"@types/prompts": "^2.0.9",
"@types/semver": "^7.3.4",
"@types/tar": "^4.0.3",
"@types/tough-cookie": "^4.0.0",
"@types/uuid": "^8.3.0",
@@ -91,6 +93,7 @@
"babel-jest": "^26.3.0",
"memfs": "^3.2.0",
"mockdate": "^3.0.2",
"nock": "^13.0.5",
"ts-node": "^8",
"typescript": "^3.9.7",
"wonka": "^4.0.14"
+27
View File
@@ -0,0 +1,27 @@
import { JSONValue } from '@expo/json-file';
import { HTTPError, RequestError } from 'got/dist/source';
export default class ApiV2Error extends RequestError {
readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(
originalError: HTTPError,
response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}
) {
super(response.message, originalError, originalError.request);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
@@ -0,0 +1,53 @@
import assert from 'assert';
import { RequestError } from 'got/dist/source';
import nock from 'nock';
import ApiV2Error from '../ApiV2Error';
import { apiClient, getExpoApiBaseUrl } from '../api';
describe('apiClient', () => {
it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl())
.post('/--/api/v2/test')
.reply(400, {
errors: [
{
message: 'hellomessage',
code: 'TEST_CODE',
stack: 'line 1: hello',
details: { who: 'world' },
metadata: { an: 'object' },
},
],
});
let error: Error;
try {
await apiClient.post('test');
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(ApiV2Error);
assert(error instanceof ApiV2Error);
expect(error.message).toEqual('hellomessage');
expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE');
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
let error: Error;
try {
await apiClient.post('test');
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(RequestError);
expect(error).not.toBeInstanceOf(ApiV2Error);
});
});
+18 -1
View File
@@ -1,5 +1,6 @@
import got, { NormalizedOptions } from 'got';
import got, { HTTPError, NormalizedOptions, RequestError } from 'got';
import ApiV2Error from './ApiV2Error';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export const apiClient = got.extend({
@@ -18,6 +19,22 @@ export const apiClient = got.extend({
}
},
],
beforeError: [
(error: RequestError): RequestError => {
if (error instanceof HTTPError) {
let result: { [key: string]: any };
try {
result = JSON.parse(error.response.body as string);
} catch (e2) {
return error;
}
if (result.errors?.length) {
return new ApiV2Error(error, result.errors[0]);
}
}
return error;
},
],
},
});
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import fs from 'fs-extra';
import log from '../../log';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { ensureValidVersions } from '../utils/updates';
@@ -11,15 +11,14 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
await fs.writeFile(
buildGradlePath,
`${buildGradleContent}\n// Integration with Expo updates\n${gradleScriptApply}\n`
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const updatedBuildGradleContents = AndroidConfig.Updates.ensureBuildGradleContainsConfigurationScript(
projectDir,
buildGradleContents
);
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
}
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
@@ -31,7 +30,7 @@ export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig)
);
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
const result = await AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
}
@@ -46,7 +45,7 @@ export async function syncUpdatesConfigurationAsync(
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
} catch (error) {
log.error(
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
@@ -64,7 +63,7 @@ export async function syncUpdatesConfigurationAsync(
}
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
log.warn(
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
@@ -72,10 +71,10 @@ export async function syncUpdatesConfigurationAsync(
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
if (!AndroidConfig.Updates.isBuildGradleConfigured(projectDir, buildGradleContents)) {
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir);
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
}
@@ -5,7 +5,7 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import log from '../../log';
import Log from '../../log';
import {
ensureAppIdentifierIsDefinedAsync,
getAndroidApplicationIdAsync,
@@ -28,10 +28,10 @@ export async function ensureApplicationIdIsValidAsync(projectDir: string) {
const applicationId = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.Android);
if (!isApplicationIdValid(applicationId)) {
const configDescription = getProjectConfigDescription(projectDir);
log.error(
Log.error(
`Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`
);
log.error(`Update "android.package" in ${configDescription} and run this command again.`);
Log.error(`Update "android.package" in ${configDescription} and run this command again.`);
throw new Error('Invalid applicationId');
}
}
@@ -47,8 +47,8 @@ export async function configureApplicationIdAsync(
if (applicationIdFromAndroidProject && applicationIdFromConfig) {
if (applicationIdFromConfig !== applicationIdFromAndroidProject) {
log.newLine();
log.warn(
Log.newLine();
Log.warn(
`We detected that your Android project is configured with a different application id than the one defined in ${configDescription}.`
);
const hasApplicationIdInStaticConfig = await hasApplicationIdInStaticConfigAsync(
@@ -56,10 +56,10 @@ export async function configureApplicationIdAsync(
exp
);
if (!hasApplicationIdInStaticConfig) {
log(`If you choose the one defined in ${configDescription} we'll automatically configure your Android project with it.
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Android project with it.
However, if you choose the one defined in the Android project you'll have to update ${configDescription} on your own.`);
}
log.newLine();
Log.newLine();
const { applicationIdSource } = await promptAsync({
type: 'select',
name: 'applicationIdSource',
@@ -102,7 +102,7 @@ However, if you choose the one defined in the Android project you'll have to upd
}
} else if (!applicationIdFromAndroidProject && applicationIdFromConfig) {
// This should never happen, adding warning just in case
log.warn(
Log.warn(
'applicationId is not specified in your ./android/app/build.gradle file. Make sure your project is configured correctly before building.'
);
return;
+5 -5
View File
@@ -8,7 +8,7 @@ import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
import { createCredentialsContextAsync } from '../../credentials/context';
import log from '../../log';
import Log from '../../log';
import { toggleConfirmAsync } from '../../prompts';
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
@@ -45,17 +45,17 @@ export async function prepareAndroidBuildAsync(
buildProfile.distribution === 'internal' &&
buildProfile.gradleCommand?.match(/bundle/)
) {
log.addNewLineIfNone();
log.warn(
Log.addNewLineIfNone();
Log.warn(
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
buildProfile.gradleCommand
)}) includes string 'bundle'.
This means that it will most likely produce an AAB and you will not be able to install it on your Android devices straight from the Expo website.`
);
log.newLine();
Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
if (!confirmed) {
log.error('Please update eas.json and come back again.');
Log.error('Please update eas.json and come back again.');
process.exit(1);
}
}
@@ -1,7 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import log from '../../log';
import Log from '../../log';
import { getAndroidApplicationIdAsync } from '../../project/projectUtils';
import { gitAddAsync } from '../../utils/git';
import { ConfigureContext } from '../context';
@@ -23,7 +23,7 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
log.withTick('Android project configured');
Log.withTick('Android project configured');
}
export async function validateAndSyncProjectConfigurationAsync(
@@ -39,6 +39,7 @@ export async function prepareJobAsync(
interface CommonJobProperties {
platform: Platform.Android;
projectArchive: ArchiveSource;
builderEnvironment: Android.BuilderEnvironment;
secrets: {
buildCredentials?: {
keystore: Android.Keystore;
@@ -72,6 +73,13 @@ async function prepareJobCommonAsync(
type: ArchiveSourceType.S3,
bucketKey: jobData.archiveBucketKey,
},
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
ndk: ctx.buildProfile.ndk,
env: ctx.buildProfile.env,
},
secrets: {
...(secretEnvs ? { secretEnvs } : {}),
...buildCredentials,
@@ -84,7 +92,8 @@ async function prepareGenericJobAsync(
jobData: JobData,
buildProfile: AndroidGenericBuildProfile
): Promise<Partial<Android.GenericJob>> {
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, jobData)),
type: Workflow.Generic,
@@ -100,7 +109,8 @@ async function prepareManagedJobAsync(
jobData: JobData,
buildProfile: AndroidManagedBuildProfile
): Promise<Partial<Android.ManagedJob>> {
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
return {
...(await prepareJobCommonAsync(ctx, jobData)),
+12 -12
View File
@@ -3,7 +3,7 @@ import { CredentialsSource } from '@expo/eas-json';
import fs from 'fs-extra';
import { apiClient } from '../api';
import log from '../log';
import Log from '../log';
import { promptAsync } from '../prompts';
import { UploadType, uploadAsync } from '../uploads';
import { createProgressTracker } from '../utils/progress';
@@ -65,13 +65,13 @@ export async function prepareBuildRequestForPlatformAsync<
}
if (!(await isGitStatusCleanAsync())) {
log.addNewLineIfNone();
const projectType = builder.ctx.platform === Platform.Android ? 'Android' : 'Xcode';
// Currently we are only updaing runtime version durring build, but if it changes in a future
// this message should also contain more info on that (or be more generic)
await reviewAndCommitChangesAsync(`Update runtime version in the ${projectType} project`, {
nonInteractive: builder.ctx.commandCtx.nonInteractive,
});
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
`[EAS Build] Run EAS Build for ${platformDisplayNames[builder.ctx.platform as Platform]}`,
{
nonInteractive: builder.ctx.commandCtx.nonInteractive,
}
);
}
const archiveBucketKey = await uploadProjectAsync(builder.ctx);
@@ -89,8 +89,8 @@ export async function prepareBuildRequestForPlatformAsync<
try {
return await withAnalyticsAsync(
async () => {
if (log.isDebug) {
log(`Starting ${platformDisplayNames[job.platform]} build`);
if (Log.isDebug) {
Log.log(`Starting ${platformDisplayNames[job.platform]} build`);
}
const {
data: { buildId, deprecationInfo },
@@ -114,7 +114,7 @@ export async function prepareBuildRequestForPlatformAsync<
} catch (error) {
const body = error?.response?.body;
if (body && JSON.parse(body)?.errors?.[0]?.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli');
}
throw error;
}
@@ -213,7 +213,7 @@ async function reviewAndCommitChangesAsync(
);
} else if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(commitMessage);
log.withTick('Committed changes.');
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await reviewAndCommitChangesAsync(commitMessage, { nonInteractive, askedFirstTime: false });
+8 -8
View File
@@ -3,7 +3,7 @@ import { EasJsonReader } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
import log from '../log';
import Log from '../log';
import { promptAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions';
import { gitAddAsync } from '../utils/git';
@@ -49,7 +49,7 @@ export async function configureAsync(options: {
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
};
log.newLine();
Log.newLine();
await ensureEasJsonExistsAsync(ctx);
if (ctx.shouldConfigureAndroid) {
await configureAndroidAsync(ctx);
@@ -59,11 +59,11 @@ export async function configureAsync(options: {
}
if (!(await isGitStatusCleanAsync())) {
log.newLine();
Log.newLine();
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
} else {
log.newLine();
log.withTick('No changes were necessary, the project is already configured correctly.');
Log.newLine();
Log.withTick('No changes were necessary, the project is already configured correctly.');
}
}
@@ -76,7 +76,7 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
await reader.validateAsync();
existingEasJson = await reader.readRawAsync();
log.withTick('Validated eas.json');
Log.withTick('Validated eas.json');
// If we have already populated eas.json with the default fields for the
// platform then proceed
@@ -122,7 +122,7 @@ export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<v
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await gitAddAsync(easJsonPath, { intentToAdd: true });
log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
Log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
}
enum ShouldCommitChanges {
@@ -153,7 +153,7 @@ async function reviewAndCommitChangesAsync(
if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(commitMessage);
log.withTick('Committed changes');
Log.withTick('Committed changes');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await showDiffAsync();
await reviewAndCommitChangesAsync(commitMessage, false);
+17 -7
View File
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import ora from 'ora';
import { apiClient } from '../api';
import log from '../log';
import Log from '../log';
import { sleep } from '../utils/promise';
import { prepareAndroidBuildAsync } from './android/build';
import { CommandContext } from './context';
@@ -17,16 +17,16 @@ export async function buildAsync(commandCtx: CommandContext): Promise<void> {
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
const scheduledBuilds = await startBuildsAsync(commandCtx);
log.newLine();
Log.newLine();
printLogsUrls(commandCtx.accountName, scheduledBuilds);
log.newLine();
Log.newLine();
if (commandCtx.waitForBuildEnd) {
const builds = await waitForBuildEndAsync(
commandCtx,
scheduledBuilds.map(i => i.buildId)
);
log.newLine();
Log.newLine();
printBuildResults(commandCtx.accountName, builds);
}
}
@@ -70,7 +70,7 @@ async function waitForBuildEndAsync(
buildIds: string[],
{ timeoutSec = 1800, intervalSec = 30 } = {}
): Promise<(Build | null)[]> {
log('Waiting for build to complete. You can press Ctrl+C to exit.');
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
const spinner = ora().start();
let time = new Date().getTime();
const endTime = time + timeoutSec * 1000;
@@ -95,6 +95,10 @@ async function waitForBuildEndAsync(
case BuildStatus.IN_QUEUE:
spinner.text = 'Build queued...';
break;
case BuildStatus.CANCELED:
spinner.text = 'Build canceled';
spinner.stopAndPersist();
return builds;
case BuildStatus.IN_PROGRESS:
spinner.text = 'Build in progress...';
break;
@@ -111,20 +115,26 @@ async function waitForBuildEndAsync(
return builds;
} else if (
builds.filter(build =>
build?.status ? [BuildStatus.FINISHED, BuildStatus.ERRORED].includes(build.status) : false
build?.status
? [BuildStatus.FINISHED, BuildStatus.ERRORED, BuildStatus.CANCELED].includes(
build.status
)
: false
).length === builds.length
) {
spinner.fail('Some of the builds failed');
spinner.fail('Some of the builds were canceled or failed.');
return builds;
} else {
const inQueue = builds.filter(build => build?.status === BuildStatus.IN_QUEUE).length;
const inProgress = builds.filter(build => build?.status === BuildStatus.IN_PROGRESS).length;
const errored = builds.filter(build => build?.status === BuildStatus.ERRORED).length;
const finished = builds.filter(build => build?.status === BuildStatus.FINISHED).length;
const canceled = builds.filter(build => build?.status === BuildStatus.CANCELED).length;
const unknownState = builds.length - inQueue - inProgress - errored - finished;
spinner.text = [
inQueue && `Builds in queue: ${inQueue}`,
inProgress && `Builds in progress: ${inProgress}`,
canceled && `Builds canceled: ${canceled}`,
errored && chalk.red(`Builds failed: ${errored}`),
finished && chalk.green(`Builds finished: ${finished}`),
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
+5 -5
View File
@@ -3,7 +3,7 @@ import { CredentialsSource } from '@expo/eas-json';
import chalk from 'chalk';
import { CredentialsProvider } from '../credentials/CredentialsProvider';
import log from '../log';
import Log from '../log';
import { confirmAsync, promptAsync } from '../prompts';
import { platformDisplayNames } from './constants';
@@ -11,7 +11,7 @@ function logCredentials(target: 'local' | 'remote', platform: Platform) {
let message = `Using ${target} ${platformDisplayNames[platform]} credentials`;
if (target === 'local') message += ` ${chalk.dim('(credentials.json)')}`;
if (target === 'remote') message += ` ${chalk.dim('(Expo server)')}`;
log.succeed(message);
Log.succeed(message);
}
async function ensureCredentialsAutoAsync(
@@ -37,7 +37,7 @@ async function ensureCredentialsAutoAsync(
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
);
} else {
log(
Log.log(
`Contents of your local credentials.json for ${platform} are not the same as credentials on EAS servers`
);
}
@@ -66,8 +66,8 @@ async function ensureCredentialsAutoAsync(
throw new Error(
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
);
} else if (log.isDebug) {
log.warn(
} else if (Log.isDebug) {
Log.warn(
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
);
}
@@ -1,24 +1,22 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import plist from '@expo/plist';
import fs from 'fs-extra';
import path from 'path';
import log from '../../log';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { gitAddAsync } from '../../utils/git';
import { ensureValidVersions } from '../utils/updates';
import { readPlistAsync, writePlistAsync } from './plist';
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
ensureValidVersions(exp);
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
projectDir,
exp,
xcodeProject
);
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
@@ -41,7 +39,7 @@ export async function syncUpdatesConfigurationAsync(
try {
await ensureUpdatesConfiguredAsync(projectDir, exp);
} catch (error) {
log.error(
Log.error(
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
);
throw error;
@@ -54,8 +52,8 @@ export async function syncUpdatesConfigurationAsync(
}
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
log.warn(
'Native project configuration is not synced with values present in you app.json, run "eas build:configure" to make sure all values are applied in the native project'
Log.warn(
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
);
}
}
@@ -63,7 +61,7 @@ export async function syncUpdatesConfigurationAsync(
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, xcodeProject)) {
const script = 'expo-updates/scripts/create-manifest-ios.sh';
const buildPhase = '"Bundle React Native code and images"';
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
@@ -77,13 +75,7 @@ async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig)
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
let expoPlist = {};
if (await fs.pathExists(expoPlistPath)) {
const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
expoPlist = plist.parse(expoPlistContent);
}
return expoPlist;
return await readPlistAsync(expoPlistPath);
}
async function writeExpoPlistAsync(
@@ -91,9 +83,6 @@ async function writeExpoPlistAsync(
expoPlist: IOSConfig.ExpoPlist
): Promise<void> {
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
const expoPlistContent = plist.build(expoPlist);
await fs.mkdirp(path.dirname(expoPlistPath));
await fs.writeFile(expoPlistPath, expoPlistContent);
await writePlistAsync(expoPlistPath, expoPlist);
await gitAddAsync(expoPlistPath, { intentToAdd: true });
}
@@ -0,0 +1,200 @@
import { ExpoConfig } from '@expo/config';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { readPlistAsync } from '../plist';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from '../version';
jest.mock('fs');
const originalConsoleLog = console.log;
beforeAll(() => {
console.log = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.removeSync(os.tmpdir());
});
beforeEach(() => {
vol.reset();
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
fs.mkdirpSync(os.tmpdir());
});
// generic workflow
describe(bumpVersionAsync, () => {
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('2');
});
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.SHORT_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
expect(fakeExp.version).toBe('1.0.1');
expect(fakeExp.ios.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.1');
expect(appJSON.expo.ios.buildNumber).toBe('1');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
expect(infoPlist['CFBundleVersion']).toBe('1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initGenericProject();
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
const infoPlist = await readPlistAsync('/repo/ios/myproject/Info.plist');
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('1');
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
expect(infoPlist['CFBundleVersion']).toBe('1');
});
});
// managed workflow
describe(bumpVersionInAppJsonAsync, () => {
it('bumps expo.ios.buildNumber when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios.buildNumber).toBe('2');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('2');
});
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.SHORT_VERSION,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('1.0.1');
expect(fakeExp.ios.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.1');
expect(appJSON.expo.ios.buildNumber).toBe('1');
});
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initManagedProject();
await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo',
exp: fakeExp,
});
const appJSON = await fs.readJSON('/repo/app.json');
expect(fakeExp.version).toBe('1.0.0');
expect(fakeExp.ios.buildNumber).toBe('1');
expect(appJSON.expo.version).toBe('1.0.0');
expect(appJSON.expo.ios.buildNumber).toBe('1');
});
});
function initGenericProject(): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: {
version: '1.0.0',
ios: {
buildNumber: '1',
},
},
}),
'./ios/myproject/Info.plist': `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>`,
},
'/repo'
);
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
ios: {
buildNumber: '1',
},
};
return fakeExp;
}
function initManagedProject(): ExpoConfig {
vol.fromJSON(
{
'./app.json': JSON.stringify({
expo: {
version: '1.0.0',
ios: {
buildNumber: '1',
},
},
}),
},
'/repo'
);
const fakeExp: ExpoConfig = {
name: 'myproject',
slug: 'myproject',
version: '1.0.0',
ios: {
buildNumber: '1',
},
};
return fakeExp;
}
+22 -18
View File
@@ -6,7 +6,7 @@ import fs from 'fs-extra';
import sortBy from 'lodash/sortBy';
import path from 'path';
import log from '../../log';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommandContext, createBuildContext } from '../context';
@@ -37,13 +37,13 @@ export async function prepareIosBuildAsync(
);
}
let iosNativeProjectScheme: string | undefined;
let iosApplicationNativeTarget: string | undefined;
let iosBuildScheme: string | undefined;
let iosApplicationTarget: string | undefined;
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
iosNativeProjectScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
iosApplicationNativeTarget = await IOSConfig.Target.getApplicationTargetForSchemeAsync(
iosBuildScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
iosApplicationTarget = await IOSConfig.BuildScheme.getApplicationTargetForSchemeAsync(
buildCtx.commandCtx.projectDir,
iosNativeProjectScheme
iosBuildScheme
);
}
@@ -52,31 +52,33 @@ export async function prepareIosBuildAsync(
return await prepareBuildRequestForPlatformAsync({
ctx: buildCtx,
projectConfiguration: {
iosNativeProjectScheme,
iosApplicationNativeTarget,
iosBuildScheme,
iosApplicationTarget,
},
ensureCredentialsAsync: ensureIosCredentialsAsync,
ensureProjectConfiguredAsync: async () => {
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
}
await validateAndSyncProjectConfigurationAsync({
projectDir: commandCtx.projectDir,
exp: commandCtx.exp,
buildProfile: buildCtx.buildProfile,
});
},
prepareJobAsync,
});
}
async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<string> {
const schemes = IOSConfig.Scheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
if (schemes.length === 1) {
return schemes[0];
}
const sortedSchemes = sortBy(schemes);
log.newLine();
log(
Log.newLine();
Log.log(
`We've found multiple schemes in your Xcode project: ${chalk.bold(sortedSchemes.join(', '))}`
);
log(
Log.log(
`You can specify the scheme you want to build at ${chalk.bold(
`builds.ios.${ctx.commandCtx.profile}.scheme`
)} in eas.json.`
@@ -84,8 +86,10 @@ async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<stri
if (ctx.commandCtx.nonInteractive) {
const withoutTvOS = sortedSchemes.filter(i => !i.includes('tvOS'));
const scheme = withoutTvOS.length > 0 ? withoutTvOS[0] : sortedSchemes[0];
log(`You've run Expo CLI in non-interactive mode, choosing the ${chalk.bold(scheme)} scheme.`);
log.newLine();
Log.log(
`You've run Expo CLI in non-interactive mode, choosing the ${chalk.bold(scheme)} scheme.`
);
Log.newLine();
return scheme;
} else {
const { selectedScheme } = await promptAsync({
@@ -94,7 +98,7 @@ async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<stri
message: 'Which scheme would you like to build now?',
choices: sortedSchemes.map(scheme => ({ title: scheme, value: scheme })),
});
log.newLine();
Log.newLine();
return selectedScheme as string;
}
}
@@ -1,16 +1,16 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import assert from 'assert';
import chalk from 'chalk';
import fs from 'fs-extra';
import log from '../../log';
import Log from '../../log';
import {
ensureAppIdentifierIsDefinedAsync,
getProjectConfigDescription,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { Platform } from '../types';
import { updateAppJsonConfigAsync } from '../utils/appJson';
enum BundleIdentiferSource {
XcodeProject,
@@ -25,10 +25,10 @@ export async function ensureBundleIdentifierIsValidAsync(projectDir: string) {
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.iOS);
if (!isBundleIdentifierValid(bundleIdentifier)) {
const configDescription = getProjectConfigDescription(projectDir);
log.error(
Log.error(
`Invalid format of iOS bundleId. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`
);
log.error(`Update "ios.bundleIdentifier" in ${configDescription} and run this command again.`);
Log.error(`Update "ios.bundleIdentifier" in ${configDescription} and run this command again.`);
throw new Error('Invalid bundleIdentifier');
}
}
@@ -38,14 +38,14 @@ export async function configureBundleIdentifierAsync(
exp: ExpoConfig
): Promise<void> {
const configDescription = getProjectConfigDescription(projectDir);
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj(
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir
);
const bundleIdentifierFromConfig = IOSConfig.BundleIdenitifer.getBundleIdentifier(exp);
const bundleIdentifierFromConfig = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
if (bundleIdentifierFromPbxproj !== bundleIdentifierFromConfig) {
log.addNewLineIfNone();
log.warn(
Log.addNewLineIfNone();
Log.warn(
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in ${configDescription}.`
);
const hasBundleIdentifierInStaticConfig = await hasBundleIdentifierInStaticConfigAsync(
@@ -53,10 +53,10 @@ export async function configureBundleIdentifierAsync(
exp
);
if (!hasBundleIdentifierInStaticConfig) {
log(`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
Log.log(`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
However, if you choose the one defined in the Xcode project you'll have to update ${configDescription} on your own.`);
}
log.newLine();
Log.newLine();
const { bundleIdentifierSource } = await promptAsync({
type: 'select',
name: 'bundleIdentifierSource',
@@ -74,7 +74,7 @@ However, if you choose the one defined in the Xcode project you'll have to updat
});
switch (bundleIdentifierSource) {
case BundleIdentiferSource.AppJson: {
IOSConfig.BundleIdenitifer.setBundleIdentifierForPbxproj(
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
projectDir,
bundleIdentifierFromConfig
);
@@ -82,7 +82,9 @@ However, if you choose the one defined in the Xcode project you'll have to updat
}
case BundleIdentiferSource.XcodeProject: {
if (hasBundleIdentifierInStaticConfig) {
await updateAppJsonConfigAsync(projectDir, exp, bundleIdentifierFromPbxproj);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
});
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
@@ -94,12 +96,14 @@ However, if you choose the one defined in the Xcode project you'll have to updat
throw new Error(missingBundleIdentifierMessage(configDescription));
} else if (bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
if (getConfigFilePaths(projectDir).staticConfigPath) {
await updateAppJsonConfigAsync(projectDir, exp, bundleIdentifierFromPbxproj);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, bundleIdentifier: bundleIdentifierFromPbxproj };
});
} else {
throw new Error(missingBundleIdentifierMessage(configDescription));
}
} else if (!bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
IOSConfig.BundleIdenitifer.setBundleIdentifierForPbxproj(
IOSConfig.BundleIdentifier.setBundleIdentifierForPbxproj(
projectDir,
bundleIdentifierFromConfig
);
@@ -110,24 +114,6 @@ function missingBundleIdentifierMessage(configDescription: string): string {
return `Please define "ios.bundleIdentifier" in ${configDescription} and run "eas build:configure" again.`;
}
async function updateAppJsonConfigAsync(
projectDir: string,
exp: ExpoConfig,
newBundleIdentifier: string
): Promise<void> {
const paths = getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic configs");
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = {
...rawStaticConfig.expo,
ios: { ...rawStaticConfig.expo?.ios, bundleIdentifier: newBundleIdentifier },
};
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
exp.ios = { ...exp.ios, bundleIdentifier: newBundleIdentifier };
}
/**
* Check if static config exists and if ios.bundleIdentifier is defined there.
* It will return false if the value in static config is different than "ios.bundleIdentifier" in ExpoConfig
+42 -14
View File
@@ -1,7 +1,9 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Workflow } from '@expo/eas-build-job';
import { VersionAutoIncrement, iOSBuildProfile } from '@expo/eas-json';
import log from '../../log';
import Log from '../../log';
import { ConfigureContext } from '../context';
import { isExpoUpdatesInstalled } from '../utils/updates';
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
@@ -9,6 +11,7 @@ import {
configureBundleIdentifierAsync,
ensureBundleIdentifierIsValidAsync,
} from './bundleIdentifer';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (!ctx.hasIosNativeProject) {
@@ -20,22 +23,47 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
if (isExpoUpdatesInstalled(ctx.projectDir)) {
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
}
log.withTick('iOS project configured');
Log.withTick('iOS project configured');
}
export async function validateAndSyncProjectConfigurationAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj(
projectDir
);
if (!bundleIdentifierFromPbxproj || bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier) {
throw new Error(
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
export async function validateAndSyncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: iOSBuildProfile;
}): Promise<void> {
const { workflow, autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement);
if (workflow === Workflow.Generic) {
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdentifier.getBundleIdentifierFromPbxproj(
projectDir
);
if (!bundleIdentifierFromPbxproj || bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier) {
throw new Error(
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
);
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
}
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp);
}
function resolveVersionBumpStrategy(autoIncrement: VersionAutoIncrement): BumpStrategy {
if (autoIncrement === true) {
return BumpStrategy.BUILD_NUMBER;
} else if (autoIncrement === false) {
return BumpStrategy.NOOP;
} else if (autoIncrement === 'buildNumber') {
return BumpStrategy.BUILD_NUMBER;
} else {
return BumpStrategy.SHORT_VERSION;
}
}
+22
View File
@@ -0,0 +1,22 @@
import { IOSConfig } from '@expo/config-plugins';
import plist from '@expo/plist';
import fs from 'fs-extra';
import path from 'path';
export async function readPlistAsync(plistPath: string): Promise<object> {
if (await fs.pathExists(plistPath)) {
const expoPlistContent = await fs.readFile(plistPath, 'utf8');
return plist.parse(expoPlistContent);
} else {
return {};
}
}
export async function writePlistAsync(
plistPath: string,
plistObject: IOSConfig.ExpoPlist | IOSConfig.InfoPlist
): Promise<void> {
const contents = plist.build(plistObject);
await fs.mkdirp(path.dirname(plistPath));
await fs.writeFile(plistPath, contents);
}
+20 -7
View File
@@ -25,8 +25,8 @@ interface JobData {
archiveBucketKey: string;
credentials?: IosCredentials;
projectConfiguration: {
iosNativeProjectScheme?: string;
iosApplicationNativeTarget?: string;
iosBuildScheme?: string;
iosApplicationTarget?: string;
};
}
@@ -48,6 +48,7 @@ export async function prepareJobAsync(
interface CommonJobProperties {
platform: Platform.iOS;
projectArchive: ArchiveSource;
builderEnvironment: iOS.BuilderEnvironment;
releaseChannel: string;
secrets: {
buildCredentials: iOS.BuildCredentials;
@@ -74,7 +75,7 @@ async function prepareJobCommonAsync(
} else if (credentials) {
// targetName
// - for managed projects: sanitized .name from the app config
// - for generic projects: name of the main application target
// - for generic projects: name of the application target
assert(targetName, 'target name should be defined');
buildCredentials = {
[targetName]: prepareTargetCredentials(credentials),
@@ -87,6 +88,14 @@ async function prepareJobCommonAsync(
type: ArchiveSourceType.S3,
bucketKey: archiveBucketKey,
},
builderEnvironment: {
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
cocoapods: ctx.buildProfile.cocoapods,
fastlane: ctx.buildProfile.fastlane,
env: ctx.buildProfile.env,
},
secrets: {
...(secretEnvs ? { secretEnvs } : {}),
buildCredentials,
@@ -109,15 +118,17 @@ async function prepareGenericJobAsync(
jobData: JobData,
buildProfile: iOSGenericBuildProfile
): Promise<Partial<iOS.GenericJob>> {
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
return {
...(await prepareJobCommonAsync(ctx, {
archiveBucketKey: jobData.archiveBucketKey,
credentials: jobData.credentials,
targetName: jobData.projectConfiguration.iosApplicationNativeTarget,
targetName: jobData.projectConfiguration.iosApplicationTarget,
})),
type: Workflow.Generic,
scheme: jobData.projectConfiguration.iosNativeProjectScheme,
scheme: jobData.projectConfiguration.iosBuildScheme,
schemeBuildConfiguration: buildProfile.schemeBuildConfiguration,
artifactPath: buildProfile.artifactPath,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
@@ -129,7 +140,8 @@ async function prepareManagedJobAsync(
jobData: JobData,
buildProfile: iOSManagedBuildProfile
): Promise<Partial<iOS.ManagedJob>> {
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
const projectRootDirectory =
path.relative(await gitRootDirectoryAsync(), ctx.commandCtx.projectDir) || '.';
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
const targetName = sanitizedName(ctx.commandCtx.exp.name);
return {
@@ -139,6 +151,7 @@ async function prepareManagedJobAsync(
targetName,
})),
type: Workflow.Managed,
buildType: buildProfile.buildType,
username: accountName,
releaseChannel: buildProfile.releaseChannel,
projectRootDirectory,
+139
View File
@@ -0,0 +1,139 @@
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { readPlistAsync, writePlistAsync } from './plist';
export enum BumpStrategy {
SHORT_VERSION,
BUILD_NUMBER,
NOOP,
}
export async function bumpVersionAsync({
bumpStrategy,
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig;
bumpStrategy: BumpStrategy;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json');
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist });
Log.log('Synchronized versions with Info.plist');
}
export async function bumpVersionInAppJsonAsync({
bumpStrategy,
projectDir,
exp,
}: {
bumpStrategy: BumpStrategy;
projectDir: string;
exp: ExpoConfig;
}): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) {
return;
}
ensureStaticConfigExists(projectDir);
Log.addNewLineIfNone();
if (bumpStrategy === BumpStrategy.SHORT_VERSION) {
const shortVersion = IOSConfig.Version.getVersion(exp);
if (semver.valid(shortVersion)) {
const bumpedShortVersion = nullthrows(semver.inc(shortVersion, 'patch'));
Log.log(
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(shortVersion)} to ${chalk.bold(
bumpedShortVersion
)}`
);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.version = bumpedShortVersion;
});
} else {
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(shortVersion)} is not a valid semver`);
const { bumpedShortVersion } = await promptAsync({
type: 'text',
name: 'bumpedShortVersion',
message: 'What is the next version?',
});
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.version = bumpedShortVersion;
});
}
} else {
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
const comps = buildNumber.split('.');
comps[comps.length - 1] = String(Number(comps[comps.length - 1]) + 1);
const bumpedBuildNumber = comps.join('.');
Log.log(
`Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold(
buildNumber
)} to ${chalk.bold(bumpedBuildNumber)}`
);
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
});
} else {
Log.log(`${chalk.bold('expo.ios.buildNumber')} = ${chalk.bold(buildNumber)} is not a number`);
const { bumpedBuildNumber } = await promptAsync({
type: 'text',
name: 'bumpedBuildNumber',
message: 'What is the next build number?',
});
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
});
}
}
}
async function readInfoPlistAsync(projectDir: string): Promise<IOSConfig.InfoPlist> {
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
return await readPlistAsync(infoPlistPath);
}
async function writeVersionsToInfoPlistAsync({
projectDir,
exp,
infoPlist,
}: {
projectDir: string;
exp: ExpoConfig;
infoPlist: IOSConfig.InfoPlist;
}): Promise<IOSConfig.InfoPlist> {
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist });
return updatedInfoPlist;
}
async function writeInfoPlistAsync({
projectDir,
infoPlist,
}: {
projectDir: string;
infoPlist: IOSConfig.InfoPlist;
}): Promise<void> {
const infoPlistPath = IOSConfig.Paths.getInfoPlistPath(projectDir);
await writePlistAsync(infoPlistPath, infoPlist);
}
function ensureStaticConfigExists(projectDir: string): void {
const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
throw new Error('autoIncrement option is not supported when using app.config.js');
}
}
+1
View File
@@ -19,6 +19,7 @@ export enum BuildStatus {
IN_PROGRESS = 'in-progress',
ERRORED = 'errored',
FINISHED = 'finished',
CANCELED = 'canceled',
}
export type TrackingContext = Record<string, string | number>;
@@ -0,0 +1,25 @@
// import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import * as ExpoConfig from '@expo/config';
import assert from 'assert';
import fs from 'fs-extra';
export async function updateAppJsonConfigAsync(
{
projectDir,
exp,
}: {
projectDir: string;
exp: ExpoConfig.ExpoConfig;
},
modifyConfig: (config: any) => void
): Promise<void> {
const paths = ExpoConfig.getConfigFilePaths(projectDir);
assert(paths.staticConfigPath, "can't update dynamic config");
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
modifyConfig(rawStaticConfig.expo);
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
modifyConfig(exp);
}
@@ -24,6 +24,8 @@ export default function formatBuild(build: Build, { accountName }: Options) {
return chalk.blue('in queue');
case BuildStatus.IN_PROGRESS:
return chalk.blue('in progress');
case BuildStatus.CANCELED:
return chalk.gray('canceled');
case BuildStatus.FINISHED:
return chalk.green('finished');
case BuildStatus.ERRORED:
@@ -41,16 +43,21 @@ export default function formatBuild(build: Build, { accountName }: Options) {
label: 'Artifact',
get value() {
if (build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS) {
return '<in progress>';
}
const url = build.artifacts?.buildUrl;
if (!url) {
return chalk.red('not found');
switch (build.status) {
case BuildStatus.IN_QUEUE:
case BuildStatus.IN_PROGRESS:
return '<in progress>';
case BuildStatus.CANCELED:
case BuildStatus.ERRORED:
return '---------';
case BuildStatus.FINISHED: {
const url = build.artifacts?.buildUrl;
return url ? url : chalk.red('not found');
}
default:
return 'unknown';
}
return url;
},
},
{ label: 'Started at', value: new Date(build.createdAt).toLocaleString() },
@@ -2,7 +2,7 @@ import { DistributionType } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import log from '../../log';
import Log from '../../log';
import { platformDisplayNames, platformEmojis } from '../constants';
import { Build } from '../types';
import { getBuildLogsUrl } from './url';
@@ -22,14 +22,14 @@ export function printLogsUrls(
buildId,
account: accountName,
});
log(`Build details: ${chalk.underline(logsUrl)}`);
Log.log(`Build details: ${chalk.underline(logsUrl)}`);
} else {
builds.forEach(({ buildId, platform }) => {
const logsUrl = getBuildLogsUrl({
buildId,
account: accountName,
});
log(`${platformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`);
Log.log(`${platformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`);
});
}
}
@@ -52,20 +52,20 @@ function printBuildResult(accountName: string, build: Build): void {
buildId: build.id,
account: accountName,
});
log(
Log.log(
`${platformEmojis[build.platform]} Open this link on your ${
platformDisplayNames[build.platform]
} devices to install the app:`
);
log(`${chalk.underline(logsUrl)}`);
log.newLine();
Log.log(`${chalk.underline(logsUrl)}`);
Log.newLine();
} else {
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
// we should account for this case better if it is possible
const url = build.artifacts?.buildUrl ?? '';
log(`${platformEmojis[build.platform]} ${platformDisplayNames[build.platform]} app:`);
log(`${chalk.underline(url)}`);
log.newLine();
Log.log(`${platformEmojis[build.platform]} ${platformDisplayNames[build.platform]} app:`);
Log.log(`${chalk.underline(url)}`);
Log.newLine();
}
}
@@ -74,17 +74,17 @@ export function printDeprecationWarnings(deprecationInfo?: DeprecationInfo): voi
return;
}
if (deprecationInfo.type === 'internal') {
log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
log.warn("Changes won't affect your project config.");
log.warn(deprecationInfo.message);
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
Log.warn("Changes won't affect your project config.");
Log.warn(deprecationInfo.message);
} else if (deprecationInfo.type === 'user-facing') {
log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
log.warn(
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
Log.warn(
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
);
log.warn(deprecationInfo.message);
Log.warn(deprecationInfo.message);
} else {
log.warn('An unexpected warning was encountered. Please report it as a bug:');
log.warn(deprecationInfo);
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
Log.warn(deprecationInfo);
}
}
+15 -12
View File
@@ -6,7 +6,7 @@ import path from 'path';
import tar from 'tar';
import { v4 as uuidv4 } from 'uuid';
import log from '../../log';
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
import {
doesGitRepoExistAsync,
@@ -28,8 +28,8 @@ async function ensureGitRepoExistsAsync(): Promise<void> {
return;
}
log.warn("It looks like you haven't initialized the git repository yet.");
log.warn('EAS Build requires you to use a git repository for your project.');
Log.warn("It looks like you haven't initialized the git repository yet.");
Log.warn('EAS Build requires you to use a git repository for your project.');
const confirmInit = await confirmAsync({
message: `Would you like to run 'git init' in the current directory?`,
@@ -41,7 +41,7 @@ async function ensureGitRepoExistsAsync(): Promise<void> {
}
await spawnAsync('git', ['init']);
log("We're going to make an initial commit for you repository.");
Log.log("We're going to make an initial commit for you repository.");
await spawnAsync('git', ['add', '-A']);
await commitPromptAsync('Initial commit');
@@ -56,9 +56,9 @@ async function maybeBailOnGitStatusAsync(): Promise<void> {
if (await isGitStatusCleanAsync()) {
return;
}
log.addNewLineIfNone();
log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
log(
Log.addNewLineIfNone();
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
Log.log(
`It's recommended to ${chalk.bold(
'commit all your changes before proceeding'
)}, so you can revert the changes made by this command if necessary.`
@@ -76,9 +76,9 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
if (await isGitStatusCleanAsync()) {
return;
}
log.addNewLineIfNone();
log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
log(
Log.addNewLineIfNone();
Log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
Log.log(
`This operation needs to be run on a clean working tree, please ${chalk.bold(
'commit all your changes before proceeding'
)}.`
@@ -111,7 +111,7 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
() => {
spinner.start();
},
log.isDebug ? 1 : 1000
Log.isDebug ? 1 : 1000
);
// TODO: Possibly warn after more time about unoptimized assets.
const compressTimerLabel = 'makeProjectTarballAsync';
@@ -159,8 +159,10 @@ async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await commitChangedFilesAsync(message);
}
// Add changed files only
async function commitChangedFilesAsync(message: string): Promise<void> {
await spawnAsync('git', ['add', '-u']);
await spawnAsync('git', ['commit', '-m', message]);
}
@@ -169,6 +171,7 @@ export {
isGitStatusCleanAsync,
showDiffAsync,
commitPromptAsync,
commitChangedFilesAsync,
ensureGitRepoExistsAsync,
ensureGitStatusIsCleanAsync,
maybeBailOnGitStatusAsync,
@@ -1,6 +1,6 @@
import { Command } from '@oclif/command';
import log from '../../log';
import Log from '../../log';
import { showLoginPromptAsync } from '../../user/actions';
export default class AccountLogin extends Command {
@@ -9,8 +9,8 @@ export default class AccountLogin extends Command {
static aliases = ['login'];
async run() {
log('Log in to EAS');
Log.log('Log in to EAS');
await showLoginPromptAsync();
log('Logged in');
Log.log('Logged in');
}
}
@@ -1,6 +1,6 @@
import { Command } from '@oclif/command';
import log from '../../log';
import Log from '../../log';
import { logoutAsync } from '../../user/User';
export default class AccountLogout extends Command {
@@ -10,6 +10,6 @@ export default class AccountLogout extends Command {
async run() {
await logoutAsync();
log('Logged out');
Log.log('Logged out');
}
}
@@ -1,7 +1,7 @@
import { Command } from '@oclif/command';
import chalk from 'chalk';
import log from '../../log';
import Log from '../../log';
import { getUserAsync } from '../../user/User';
import { getActorDisplayName } from '../../user/actions';
@@ -13,9 +13,9 @@ export default class AccountView extends Command {
async run() {
const user = await getUserAsync();
if (user) {
log(chalk.green(getActorDisplayName(user)));
Log.log(chalk.green(getActorDisplayName(user)));
} else {
log.warn('Not logged in');
Log.warn('Not logged in');
process.exit(1);
}
}
@@ -4,71 +4,71 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { UpdateRelease } from '../../graphql/generated';
import log from '../../log';
import { UpdateBranch } from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { getBranchNameAsync } from '../../utils/git';
export async function createUpdateReleaseOnAppAsync({
export async function createUpdateBranchOnAppAsync({
appId,
releaseName,
branchName,
}: {
appId: string;
releaseName: string;
}): Promise<Pick<UpdateRelease, 'id' | 'releaseName'>> {
branchName: string;
}): Promise<Pick<UpdateBranch, 'id' | 'branchName'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
{ updateRelease: { createUpdateReleaseForApp: Pick<UpdateRelease, 'id' | 'releaseName'> } },
{ appId: string; releaseName: string }
{ updateBranch: { createUpdateBranchForApp: Pick<UpdateBranch, 'id' | 'branchName'> } },
{ appId: string; branchName: string }
>(
gql`
mutation CreateUpdateReleaseForApp($appId: ID!, $releaseName: String!) {
updateRelease {
createUpdateReleaseForApp(appId: $appId, releaseName: $releaseName) {
mutation createUpdateBranchForApp($appId: ID!, $branchName: String!) {
updateBranch {
createUpdateBranchForApp(appId: $appId, branchName: $branchName) {
id
releaseName
branchName
}
}
}
`,
{
appId,
releaseName,
branchName,
}
)
.toPromise()
);
return data.updateRelease.createUpdateReleaseForApp;
return data.updateBranch.createUpdateBranchForApp;
}
export default class ReleaseCreate extends Command {
export default class BranchCreate extends Command {
static hidden = true;
static description = 'Create a release on the current project.';
static description = 'Create a branch on the current project.';
static args = [
{
name: 'releaseName',
name: 'branchName',
required: false,
description: 'Name of the release to create',
description: 'Name of the branch to create',
},
];
static flags = {
json: flags.boolean({
description: 'return a json with the new release ID and name.',
description: 'return a json with the new branch ID and name.',
default: false,
}),
};
async run() {
let {
args: { releaseName },
args: { branchName },
flags,
} = this.parse(ReleaseCreate);
} = this.parse(BranchCreate);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
@@ -82,30 +82,30 @@ export default class ReleaseCreate extends Command {
projectName: exp.slug,
});
if (!releaseName) {
const validationMessage = 'Release name may not be empty.';
if (!branchName) {
const validationMessage = 'Branch name may not be empty.';
if (flags.json) {
throw new Error(validationMessage);
}
({ releaseName } = await promptAsync({
({ branchName } = await promptAsync({
type: 'text',
name: 'releaseName',
message: 'Please name the release:',
name: 'branchName',
message: 'Please name the branch:',
initial:
(await getBranchNameAsync()) || `release-${Math.random().toString(36).substr(2, 4)}`,
(await getBranchNameAsync()) || `branch-${Math.random().toString(36).substr(2, 4)}`,
validate: value => (value ? true : validationMessage),
}));
}
const newRelease = await createUpdateReleaseOnAppAsync({ appId: projectId, releaseName });
const newBranch = await createUpdateBranchOnAppAsync({ appId: projectId, branchName });
if (flags.json) {
log(newRelease);
Log.log(newBranch);
return;
}
log.withTick(
`️Created a new release: ${chalk.bold(newRelease.releaseName)} on project ${chalk.bold(
Log.withTick(
`️Created a new branch: ${chalk.bold(newBranch.branchName)} on project ${chalk.bold(
`@${accountName}/${exp.slug}`
)}.`
);
@@ -5,17 +5,17 @@ import gql from 'graphql-tag';
import { format } from 'timeago.js';
import { graphqlClient } from '../../graphql/client';
import { RootQuery, Update, UpdateRelease } from '../../graphql/generated';
import log from '../../log';
import { RootQuery, Update, UpdateBranch } from '../../graphql/generated';
import Log from '../../log';
import { findProjectRootAsync, getProjectFullNameAsync } from '../../project/projectUtils';
import { getActorDisplayName } from '../../user/actions';
const RELEASES_LIMIT = 10_000;
const BRANCHES_LIMIT = 10_000;
export default class ReleaseList extends Command {
export default class BranchList extends Command {
static hidden = true;
static description = 'List all releases on this project.';
static description = 'List all branches on this project.';
static flags = {
json: flags.boolean({
@@ -25,43 +25,43 @@ export default class ReleaseList extends Command {
};
async run() {
const { flags } = this.parse(ReleaseList);
const { flags } = this.parse(BranchList);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
const fullName = await getProjectFullNameAsync(projectDir);
const releases = await this.listReleasesAsync({ fullName });
const branches = await this.listBranchesAsync({ fullName });
if (flags.json) {
log(JSON.stringify(releases, null, 2));
Log.log(JSON.stringify(branches, null, 2));
} else {
const table = new CliTable({ head: ['Release', 'Latest update'] });
const table = new CliTable({ head: ['Branch', 'Latest update'] });
table.push(
...releases.map((release: UpdateRelease) => [
release.releaseName,
formatUpdate(release.updates[0]),
...branches.map((branch: UpdateBranch) => [
branch.branchName,
formatUpdate(branch.updates[0]),
])
);
log(table.toString());
if (releases.length >= RELEASES_LIMIT) {
log.warn(`Showing first ${RELEASES_LIMIT} releases, some results might be omitted.`);
Log.log(table.toString());
if (branches.length >= BRANCHES_LIMIT) {
Log.warn(`Showing first ${BRANCHES_LIMIT} branches, some results might be omitted.`);
}
}
}
async listReleasesAsync({ fullName }: { fullName: string }): Promise<UpdateRelease[]> {
async listBranchesAsync({ fullName }: { fullName: string }): Promise<UpdateBranch[]> {
const { data, error } = await graphqlClient
.query<RootQuery>(
gql`
query ReleasesByAppQuery($fullName: String!, $limit: Int!) {
query BranchesByAppQuery($fullName: String!, $limit: Int!) {
app {
byFullName(fullName: $fullName) {
id
fullName
updateReleases(offset: 0, limit: $limit) {
updateBranches(offset: 0, limit: $limit) {
id
releaseName
branchName
updates(offset: 0, limit: 1) {
id
actor {
@@ -84,20 +84,20 @@ export default class ReleaseList extends Command {
`,
{
fullName,
limit: RELEASES_LIMIT,
limit: BRANCHES_LIMIT,
}
)
.toPromise();
if (error) {
if (error.networkError) {
throw new CLIError(`Fetching releases failed: ${error.networkError.message}`);
throw new CLIError(`Fetching branches failed: ${error.networkError.message}`);
} else {
throw new CLIError(error.graphQLErrors.map(e => e.message).join('\n'));
}
}
return data?.app?.byFullName.updateReleases ?? [];
return data?.app?.byFullName.updateBranches ?? [];
}
}
@@ -5,19 +5,20 @@ import Table from 'cli-table3';
import ora from 'ora';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import log from '../../log';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectAccountNameAsync,
getReleaseByNameAsync,
} from '../../project/projectUtils';
import { buildUpdateInfoGroupAsync, collectAssets, uploadAssetsAsync } from '../../project/publish';
import { promptAsync } from '../../prompts';
import { getLastCommitMessageAsync } from '../../utils/git';
export default class ReleasePublish extends Command {
static description = 'Publish an update group to a release.';
export default class BranchPublish extends Command {
static hidden = true;
static description = 'Publish an update group to a branch.';
static flags = {
'input-dir': flags.string({
@@ -25,28 +26,23 @@ export default class ReleasePublish extends Command {
default: 'dist',
required: false,
}),
release: flags.string({
description: 'current name of the release.',
branch: flags.string({
description: 'current name of the branch.',
}),
message: flags.string({
description: 'Short message describing the updates.',
required: false,
}),
json: flags.boolean({
description: `return a json with the edited release's ID and name.`,
description: `return a json with the edited branch's ID and name.`,
default: false,
}),
};
async run() {
let {
flags: {
json: jsonFlag,
release: releaseName,
message: updateMessage,
'input-dir': inputDir,
},
} = this.parse(ReleasePublish);
flags: { json: jsonFlag, branch: branchName, message: updateMessage, 'input-dir': inputDir },
} = this.parse(BranchPublish);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
@@ -67,15 +63,15 @@ export default class ReleasePublish extends Command {
projectName: slug,
});
if (!releaseName) {
const validationMessage = 'release name may not be empty.';
if (!branchName) {
const validationMessage = 'branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ releaseName } = await promptAsync({
({ branchName } = await promptAsync({
type: 'text',
name: 'releaseName',
message: 'Please enter the name of the release to publish on:',
name: 'branchName',
message: 'Please enter the name of the branch to publish on:',
validate: value => (value ? true : validationMessage),
}));
}
@@ -94,9 +90,9 @@ export default class ReleasePublish extends Command {
}));
}
const { id: releaseId } = await getReleaseByNameAsync({
const { id: branchId } = await getBranchByNameAsync({
appId: projectId,
releaseName: releaseName!,
branchName: branchName!,
});
let updateInfoGroup;
@@ -115,7 +111,7 @@ export default class ReleasePublish extends Command {
const publishSpinner = ora('Publishing...').start();
try {
newUpdateGroup = await PublishMutation.publishUpdateGroupAsync({
releaseId,
branchId,
updateInfoGroup,
runtimeVersion,
updateMessage,
@@ -127,7 +123,7 @@ export default class ReleasePublish extends Command {
}
if (jsonFlag) {
log(newUpdateGroup);
Log.log(newUpdateGroup);
} else {
const outputMessage = new Table({
wordWrap: true,
@@ -152,12 +148,12 @@ export default class ReleasePublish extends Command {
});
outputMessage.push(
[chalk.dim('project'), `@${accountName}/${slug}`],
[chalk.dim('release'), releaseName],
[chalk.dim('branch'), branchName],
[chalk.dim('runtimeVersion'), runtimeVersion],
[chalk.dim('updateGroupID'), newUpdateGroup.updateGroup],
[chalk.dim('message'), updateMessage]
);
log(outputMessage.toString());
Log.log(outputMessage.toString());
}
}
}
@@ -4,14 +4,14 @@ import chalk from 'chalk';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { EditUpdateReleaseInput, UpdateRelease } from '../../graphql/generated';
import log from '../../log';
import { EditUpdateBranchInput, UpdateBranch } from '../../graphql/generated';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountName } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
async function renameUpdateReleaseOnAppAsync({
async function renameUpdateBranchOnAppAsync({
appId,
currentName,
newName,
@@ -19,21 +19,21 @@ async function renameUpdateReleaseOnAppAsync({
appId: string;
currentName: string;
newName: string;
}): Promise<UpdateRelease> {
}): Promise<UpdateBranch> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
{ updateRelease: { editUpdateRelease: UpdateRelease } },
{ updateBranch: { editUpdateBranch: UpdateBranch } },
{
input: EditUpdateReleaseInput;
input: EditUpdateBranchInput;
}
>(
gql`
mutation EditUpdateRelease($input: EditUpdateReleaseInput!) {
updateRelease {
editUpdateRelease(input: $input) {
mutation EditUpdateBranch($input: EditUpdateBranchInput!) {
updateBranch {
editUpdateBranch(input: $input) {
id
releaseName
branchName
}
}
}
@@ -41,31 +41,31 @@ async function renameUpdateReleaseOnAppAsync({
{
input: {
appId,
releaseName: currentName,
newReleaseName: newName,
branchName: currentName,
newBranchName: newName,
},
}
)
.toPromise()
);
return data.updateRelease.editUpdateRelease;
return data.updateBranch.editUpdateBranch;
}
export default class ReleaseRename extends Command {
export default class BranchRename extends Command {
static hidden = true;
static description = 'Rename a release.';
static description = 'Rename a branch.';
static flags = {
from: flags.string({
description: 'current name of the release.',
description: 'current name of the branch.',
required: false,
}),
to: flags.string({
description: 'new name of the release.',
description: 'new name of the branch.',
required: false,
}),
json: flags.boolean({
description: `return a json with the edited release's ID and name.`,
description: `return a json with the edited branch's ID and name.`,
default: false,
}),
};
@@ -73,7 +73,7 @@ export default class ReleaseRename extends Command {
async run() {
let {
flags: { json: jsonFlag, from: currentName, to: newName },
} = this.parse(ReleaseRename);
} = this.parse(BranchRename);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
@@ -94,7 +94,7 @@ export default class ReleaseRename extends Command {
({ currentName } = await promptAsync({
type: 'text',
name: 'currentName',
message: 'Please enter the current name of the release to rename:',
message: 'Please enter the current name of the branch to rename:',
validate: value => (value ? true : validationMessage),
}));
}
@@ -112,20 +112,20 @@ export default class ReleaseRename extends Command {
}));
}
const editedRelease = await renameUpdateReleaseOnAppAsync({
const editedBranch = await renameUpdateBranchOnAppAsync({
appId: projectId,
currentName: currentName!,
newName: newName!,
});
if (jsonFlag) {
log(editedRelease);
Log.log(editedBranch);
return;
}
log.withTick(
`️Renamed release from ${currentName} to ${chalk.bold(
editedRelease.releaseName
Log.withTick(
`️Renamed branch from ${currentName} to ${chalk.bold(
editedBranch.branchName
)} on project ${chalk.bold(`@${accountName}/${exp.slug}`)}.`
);
}
@@ -7,7 +7,7 @@ import { groupBy } from 'lodash';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { Update } from '../../graphql/generated';
import log from '../../log';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -16,15 +16,15 @@ const PAGE_LIMIT = 10_000;
type TruncatedUpdate = Pick<Update, 'updateGroup' | 'updateMessage' | 'createdAt' | 'actor'>;
async function viewUpdateReleaseAsync({
async function viewUpdateBranchAsync({
appId,
releaseName,
branchName,
}: {
appId: string;
releaseName: string;
branchName: string;
}): Promise<{
id: string;
releaseName: string;
branchName: string;
updates: TruncatedUpdate[];
}> {
const data = await withErrorHandlingAsync(
@@ -33,9 +33,9 @@ async function viewUpdateReleaseAsync({
{
app: {
byId: {
updateReleaseByReleaseName: {
updateBranchByBranchName: {
id: string;
releaseName: string;
branchName: string;
updates: TruncatedUpdate[];
};
};
@@ -43,18 +43,18 @@ async function viewUpdateReleaseAsync({
},
{
appId: string;
releaseName: string;
branchName: string;
limit: number;
}
>(
gql`
query ViewRelease($appId: String!, $releaseName: String!, $limit: Int!) {
query ViewBranch($appId: String!, $branchName: String!, $limit: Int!) {
app {
byId(appId: $appId) {
id
updateReleaseByReleaseName(releaseName: $releaseName) {
updateBranchByBranchName(branchName: $branchName) {
id
releaseName
branchName
updates(offset: 0, limit: $limit) {
id
updateGroup
@@ -77,39 +77,39 @@ async function viewUpdateReleaseAsync({
`,
{
appId,
releaseName,
branchName,
limit: PAGE_LIMIT,
}
)
.toPromise()
);
return data.app.byId.updateReleaseByReleaseName;
return data.app.byId.updateBranchByBranchName;
}
export default class ReleaseView extends Command {
export default class BranchView extends Command {
static hidden = true;
static description = 'View a release.';
static description = 'View a branch.';
static args = [
{
name: 'releaseName',
name: 'branchName',
required: false,
description: 'Name of the release to view',
description: 'Name of the branch to view',
},
];
static flags = {
json: flags.boolean({
description: `return a json with the release's ID name and recent update groups.`,
description: `return a json with the branch's ID name and recent update groups.`,
default: false,
}),
};
async run() {
let {
args: { releaseName },
args: { branchName },
flags: { json: jsonFlag },
} = this.parse(ReleaseView);
} = this.parse(BranchView);
const projectDir = await findProjectRootAsync(process.cwd());
if (!projectDir) {
@@ -124,30 +124,30 @@ export default class ReleaseView extends Command {
projectName: slug,
});
if (!releaseName) {
const validationMessage = 'Release name may not be empty.';
if (!branchName) {
const validationMessage = 'Branch name may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ releaseName } = await promptAsync({
({ branchName } = await promptAsync({
type: 'text',
name: 'releaseName',
message: 'Please enter the name of the release to view:',
name: 'branchName',
message: 'Please enter the name of the branch to view:',
validate: value => (value ? true : validationMessage),
}));
}
const UpdateRelease = await viewUpdateReleaseAsync({
const UpdateBranch = await viewUpdateBranchAsync({
appId: projectId,
releaseName,
branchName,
});
const updates = Object.values(groupBy(UpdateRelease.updates, u => u.updateGroup)).map(
const updates = Object.values(groupBy(UpdateBranch.updates, u => u.updateGroup)).map(
updateGroup => updateGroup[0]
);
if (jsonFlag) {
log({ ...UpdateRelease, updates });
Log.log({ ...UpdateBranch, updates });
return;
}
@@ -165,12 +165,12 @@ export default class ReleaseView extends Command {
]);
}
log.withTick(
`️Release: ${chalk.bold(UpdateRelease.releaseName)} on project ${chalk.bold(
Log.withTick(
`️Branch: ${chalk.bold(UpdateBranch.branchName)} on project ${chalk.bold(
`@${accountName}/${slug}`
)}. Release ID: ${chalk.bold(UpdateRelease.id)}`
)}. Branch ID: ${chalk.bold(UpdateBranch.id)}`
);
log(chalk.bold('Recent update groups published on this release:'));
log(updateGroupTable.toString());
Log.log(chalk.bold('Recent update groups published on this branch:'));
Log.log(updateGroupTable.toString());
}
}
@@ -0,0 +1,138 @@
import { Command } from '@oclif/command';
import chalk from 'chalk';
import gql from 'graphql-tag';
import ora from 'ora';
import { platformEmojis } from '../../build/constants';
import { Platform } from '../../build/types';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppPlatform, Build, BuildStatus } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectFullNameAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../prompts';
async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<{ build: { cancel: { id: string; status: BuildStatus } } }>(
gql`
mutation CancelBuildMutation($buildId: ID!) {
build(buildId: $buildId) {
cancel {
id
status
}
}
}
`,
{ buildId }
)
.toPromise()
);
return data.build!.cancel;
}
const appPlatformMap = {
[AppPlatform.Android]: Platform.Android,
[AppPlatform.Ios]: Platform.iOS,
};
function formatUnfinishedBuild(
build: Pick<Build, 'id' | 'platform' | 'status' | 'createdAt'>
): string {
const platform = platformEmojis[appPlatformMap[build.platform]];
const startTime = new Date(build.createdAt).toLocaleString();
const status = chalk.blue(build.status === BuildStatus.InQueue ? 'in-queue' : 'in-progress');
return `${platform} Started at: ${startTime}, Status: ${status}, Id: ${build.id}`;
}
async function selectBuildToCancelAsync(
projectId: string,
projectFullName: string
): Promise<string | null> {
const spinner = ora().start('Fetching the uncompleted builds…');
let builds;
try {
const [inQueueBuilds, inProgressBuilds] = await Promise.all([
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InQueue }),
BuildQuery.allForAppAsync(projectId, { status: BuildStatus.InProgress }),
]);
spinner.stop();
builds = [...inQueueBuilds, ...inProgressBuilds];
} catch (error) {
spinner.fail(
`Something went wrong and we couldn't fetch the builds for the project ${projectFullName}.`
);
throw error;
}
if (builds.length === 0) {
Log.warn(`There aren't any uncompleted builds for the project ${projectFullName}.`);
return null;
} else if (builds.length === 1) {
Log.log('Found one build');
Log.log(formatUnfinishedBuild(builds[0]));
await confirmAsync({
message: 'Do you want to cancel it?',
});
return builds[0].id;
} else {
const buildId = await selectAsync<string>(
'Which build do you want to cancel?',
builds.map(build => ({
title: formatUnfinishedBuild(build),
value: build.id,
}))
);
return buildId;
}
}
async function ensureBuildExistsAsync(buildId: string): Promise<void> {
try {
await BuildQuery.byIdAsync(buildId);
} catch (err) {
throw new Error(`Couldn't find a build matching the id ${buildId}`);
}
}
export default class BuildCancel extends Command {
static description = 'Cancel a build.';
static args = [{ name: 'BUILD_ID' }];
async run() {
const { BUILD_ID: buildIdFromArg } = this.parse(BuildCancel).args;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectId = await getProjectIdAsync(projectDir);
const projectFullName = await getProjectFullNameAsync(projectDir);
if (buildIdFromArg) {
await ensureBuildExistsAsync(buildIdFromArg);
}
const buildId = buildIdFromArg || (await selectBuildToCancelAsync(projectId, projectFullName));
if (!buildId) {
return;
}
const spinner = ora().start('Canceling the build…');
try {
const { status } = await cancelBuildAsync(buildId);
if (status === BuildStatus.Canceled) {
spinner.succeed('Build canceled');
} else {
spinner.text = 'Build is already completed';
spinner.stopAndPersist();
}
} catch (error) {
spinner.fail(`Something went wrong and we couldn't cancel your build ${buildId}`);
throw error;
}
}
}
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { configureAsync } from '../../build/configure';
import { RequestedPlatform } from '../../build/types';
import log, { learnMore } from '../../log';
import Log, { learnMore } from '../../log';
import { findProjectRootAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
@@ -25,17 +25,17 @@ export default class BuildConfigure extends Command {
async run() {
const { flags } = this.parse(BuildConfigure);
log(
Log.log(
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
);
log.newLine();
Log.newLine();
const platform =
(flags.platform as RequestedPlatform | undefined) ?? (await promptForPlatformAsync());
const allowExperimental = flags['allow-experimental'];
if (allowExperimental) {
log.warn(
Log.warn(
`Project configuration will execute some additional steps that might fail if structure of your native project is significantly different from ${chalk.bold(
'expo eject'
)} or ${chalk.bold('expo init')}`
@@ -49,7 +49,7 @@ export default class BuildConfigure extends Command {
projectDir: (await findProjectRootAsync()) ?? process.cwd(),
});
log.newLine();
Log.newLine();
logSuccess(platform);
}
}
@@ -66,7 +66,7 @@ function logSuccess(platform: RequestedPlatform) {
storesText = 'the Apple App Store';
}
log(`🎉 Your ${platformsText} ready to build.
Log.log(`🎉 Your ${platformsText} ready to build.
- Run ${chalk.bold('eas build')} when you are ready to create your first build.
- Once the build is completed, run ${chalk.bold('eas submit')} to upload the app to ${storesText}
+6 -5
View File
@@ -14,7 +14,7 @@ import formatBuild from '../../build/utils/formatBuild';
import { isGitStatusCleanAsync } from '../../build/utils/repository';
import { AppPlatform } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import log, { learnMore } from '../../log';
import Log, { learnMore } from '../../log';
import {
isEasEnabledForProjectAsync,
warnEasUnavailable,
@@ -51,6 +51,7 @@ export default class Build extends Command {
}),
wait: flags.boolean({
default: true,
allowNo: true,
description: 'Wait for build(s) to complete',
}),
};
@@ -131,11 +132,11 @@ async function ensureNoPendingBuildsExistAsync({
);
const pendingBuilds = maybePendingBuilds.filter(i => i !== null);
if (pendingBuilds.length > 0) {
log.newLine();
log.error(
Log.newLine();
Log.error(
'Your other builds are still pending. Wait for them to complete before running this command again.'
);
log.newLine();
Log.newLine();
const results = await Promise.all(
pendingBuilds.map(pendingBuild => {
return apiClient.get<{ data: BuildType }>(
@@ -148,7 +149,7 @@ async function ensureNoPendingBuildsExistAsync({
);
for (const result of results) {
log(formatBuild(result.body.data, { accountName }));
Log.log(formatBuild(result.body.data, { accountName }));
}
process.exitCode = 1;
+3 -3
View File
@@ -5,7 +5,7 @@ import ora from 'ora';
import { apiClient } from '../../api';
import { Build } from '../../build/types';
import formatBuild from '../../build/utils/formatBuild';
import log from '../../log';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -18,7 +18,7 @@ export default class BuildList extends Command {
static flags = {
platform: flags.enum({ options: ['all', 'android', 'ios'] }),
status: flags.enum({ options: ['in-queue', 'in-progress', 'errored', 'finished'] }),
status: flags.enum({ options: ['in-queue', 'in-progress', 'errored', 'finished', 'canceled'] }),
limit: flags.integer(),
};
@@ -56,7 +56,7 @@ export default class BuildList extends Command {
.map(build => formatBuild(build, { accountName }))
.join(`\n\n${chalk.dim('———')}\n\n`);
log(`\n${list}`);
Log.log(`\n${list}`);
} else {
spinner.fail(`Couldn't find any builds for the project ${projectName}`);
}
+4 -4
View File
@@ -5,7 +5,7 @@ import ora from 'ora';
import { apiClient } from '../../api';
import { Build } from '../../build/types';
import formatBuild from '../../build/utils/formatBuild';
import log from '../../log';
import Log from '../../log';
import {
findProjectRootAsync,
getProjectAccountNameAsync,
@@ -16,10 +16,10 @@ import {
export default class BuildView extends Command {
static description = 'view a build for your project';
static args = [{ name: 'buildId' }];
static args = [{ name: 'BUILD_ID' }];
async run() {
const { buildId } = this.parse(BuildView).args;
const { BUILD_ID: buildId } = this.parse(BuildView).args;
const projectDir = (await findProjectRootAsync()) ?? process.cwd();
const projectId = await getProjectIdAsync(projectDir);
@@ -58,7 +58,7 @@ export default class BuildView extends Command {
spinner.succeed(`Showing the last build for the project ${projectName}`);
}
log(`\n${formatBuild(build, { accountName })}`);
Log.log(`\n${formatBuild(build, { accountName })}`);
} catch (e) {
const error = e as HTTPError;
+28 -28
View File
@@ -5,51 +5,51 @@ import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { UpdateChannel } from '../../graphql/generated';
import log from '../../log';
import Log from '../../log';
import { ensureProjectExistsAsync } from '../../project/ensureProjectExists';
import {
findProjectRootAsync,
getBranchByNameAsync,
getProjectAccountNameAsync,
getReleaseByNameAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { createUpdateReleaseOnAppAsync } from '../release/create';
import { createUpdateBranchOnAppAsync } from '../branch/create';
async function createUpdateChannelOnAppAsync({
appId,
channelName,
releaseId,
branchId,
}: {
appId: string;
channelName: string;
releaseId: string;
branchId: string;
}): Promise<UpdateChannel> {
// Point the new channel at a release with its same name.
const releaseMapping = JSON.stringify({
data: [{ releaseId, releaseMappingLogic: 'true' }],
// Point the new channel at a branch with its same name.
const branchMapping = JSON.stringify({
data: [{ branchId, branchMappingLogic: 'true' }],
version: 0,
});
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
{ updateChannel: { createUpdateChannelForApp: UpdateChannel } },
{ appId: string; channelName: string; releaseMapping: string }
{ appId: string; channelName: string; branchMapping: string }
>(
gql`
mutation CreateUpdateChannelForApp(
$appId: ID!
$channelName: String!
$releaseMapping: String!
$branchMapping: String!
) {
updateChannel {
createUpdateChannelForApp(
appId: $appId
channelName: $channelName
releaseMapping: $releaseMapping
branchMapping: $branchMapping
) {
id
channelName
releaseMapping
branchMapping
}
}
}
@@ -57,7 +57,7 @@ async function createUpdateChannelOnAppAsync({
{
appId,
channelName,
releaseMapping,
branchMapping,
}
)
.toPromise()
@@ -80,7 +80,7 @@ export default class ChannelCreate extends Command {
static flags = {
json: flags.boolean({
description:
'print output as a JSON object with the new channel ID, name and release mapping.',
'print output as a JSON object with the new channel ID, name and branch mapping.',
default: false,
}),
};
@@ -117,39 +117,39 @@ export default class ChannelCreate extends Command {
}));
}
let releaseId: string;
let releaseMessage: string;
let branchId: string;
let branchMessage: string;
try {
const existingRelease = await getReleaseByNameAsync({
const existingBranch = await getBranchByNameAsync({
appId: projectId,
releaseName: channelName,
branchName: channelName,
});
releaseId = existingRelease.id;
releaseMessage = `We found a release with the same name`;
branchId = existingBranch.id;
branchMessage = `We found a branch with the same name`;
} catch (e) {
const newRelease = await createUpdateReleaseOnAppAsync({
const newBranch = await createUpdateBranchOnAppAsync({
appId: projectId,
releaseName: channelName,
branchName: channelName,
});
releaseId = newRelease.id;
releaseMessage = `We also went ahead and made a release with the same name`;
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
}
const newChannel = await createUpdateChannelOnAppAsync({
appId: projectId,
channelName,
releaseId,
branchId,
});
if (jsonFlag) {
log(newChannel);
Log.log(newChannel);
return;
}
log.withTick(
Log.withTick(
`️Created a new channel ${chalk.bold(newChannel.channelName)} on project ${chalk.bold(
`@${accountName}/${slug}`
)}. ${releaseMessage} and have pointed the channel at it. You can now update your app by publishing!`
)}. ${branchMessage} and have pointed the channel at it. You can now update your app by publishing!`
);
}
}
+2 -2
View File
@@ -6,7 +6,7 @@ import ora from 'ora';
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
import formatDevice from '../../devices/utils/formatDevice';
import log from '../../log';
import Log from '../../log';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
@@ -79,7 +79,7 @@ export default class BuildList extends Command {
)
.join(`\n\n${chalk.dim('———')}\n\n`);
log(`\n${list}`);
Log.log(`\n${list}`);
} else {
spinner.fail(`Couldn't find any devices for the team ${appleTeamIdentifier}`);
}
+3 -3
View File
@@ -3,7 +3,7 @@ import ora from 'ora';
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import formatDevice from '../../devices/utils/formatDevice';
import log from '../../log';
import Log from '../../log';
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
export default class DeviceView extends Command {
@@ -15,7 +15,7 @@ export default class DeviceView extends Command {
const { UDID } = this.parse(DeviceView).args;
if (!UDID) {
log(
Log.log(
`The device UDID is required to view a specific device. For example:
eas device:view 00005787-000872430189501D
@@ -38,7 +38,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
if (device) {
spinner.succeed('Fetched device details');
log(`\n${formatDevice(device, device.appleTeam)}`);
Log.log(`\n${formatDevice(device, device.appleTeam)}`);
} else {
spinner.fail(`Couldn't find a device with the UDID ${UDID}`);
}
@@ -0,0 +1,17 @@
export function getAppstoreMock() {
return {
ensureAuthenticatedAsync: jest.fn(),
ensureBundleIdExistsAsync: jest.fn(),
listDistributionCertificatesAsync: jest.fn(),
createDistributionCertificateAsync: jest.fn(),
revokeDistributionCertificateAsync: jest.fn(),
listPushKeysAsync: jest.fn(),
createPushKeyAsync: jest.fn(),
revokePushKeyAsync: jest.fn(),
useExistingProvisioningProfileAsync: jest.fn(),
listProvisioningProfilesAsync: jest.fn(),
createProvisioningProfileAsync: jest.fn(),
revokeProvisioningProfileAsync: jest.fn(),
createOrReuseAdhocProvisioningProfileAsync: jest.fn(),
};
}
@@ -3,17 +3,23 @@ import merge from 'lodash/merge';
import { CredentialsManager } from '../CredentialsManager';
import { Context } from '../context';
import { getAndroidApiMockWithoutCredentials } from './fixtures-android';
import { getAppstoreMock } from './fixtures-appstore';
import { testAppJson, testUsername } from './fixtures-constants';
import { getIosApiMockWithoutCredentials } from './fixtures-ios';
import { getNewIosApiMockWithoutCredentials } from './fixtures-new-ios';
export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
const defaultMock = {
ios: getIosApiMockWithoutCredentials(),
newIos: getNewIosApiMockWithoutCredentials(),
android: getAndroidApiMockWithoutCredentials(),
appStore: jest.fn(),
appStore: getAppstoreMock(),
ensureAppleCtx: jest.fn(),
ensureProjectContext: jest.fn(),
user: {
__typename: 'User',
username: testUsername,
accounts: [{ id: 'test-account-id', name: testUsername }],
},
hasAppleCtx: jest.fn(() => true),
hasProjectContext: true,
@@ -1,3 +1,9 @@
import {
AppFragment,
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleTeamFragment,
} from '../../graphql/generated';
import {
DistributionCertificate,
DistributionCertificateStoreInfo,
@@ -34,6 +40,61 @@ export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
};
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
export const testAppleTeamFragment: AppleTeamFragment = {
id: 'test-apple-team-id-1',
appleTeamIdentifier: 'test-apple-team-identifier',
};
export const testAppleAppIdentifierFragment: AppleAppIdentifierFragment = {
id: 'test-apple-app-identifier-id-1',
bundleIdentifier: 'foo.bar.com',
};
export const testAppFragment: AppFragment = {
id: 'test-app-id',
fullName: '@testuser/testapp',
slug: 'testapp',
};
export const testDistCertFragmentNoDependencies: AppleDistributionCertificateFragment = {
id: 'test-dist-cert-id-1',
certificateP12: 'Y2VydHAxMg==',
certificatePassword: 'test-password',
serialNumber: 'test-serial',
developerPortalIdentifier: 'test-developer-identifier',
validityNotAfter: new Date(),
validityNotBefore: new Date(),
updatedAt: new Date(),
appleTeam: { ...testAppleTeamFragment },
iosAppBuildCredentialsList: [],
};
export const testDistCertFragmentOneDependency: AppleDistributionCertificateFragment = {
id: 'test-dist-cert-id-1',
certificateP12: 'Y2VydHAxMg==',
certificatePassword: 'test-password',
serialNumber: 'test-serial',
developerPortalIdentifier: 'test-developer-identifier',
validityNotAfter: new Date(),
validityNotBefore: new Date(),
updatedAt: new Date(),
appleTeam: { ...testAppleTeamFragment },
iosAppBuildCredentialsList: [
{
id: 'test-build-credentials-id',
iosAppCredentials: {
id: 'test-app-credentials-id',
appleAppIdentifier: { ...testAppleAppIdentifierFragment },
app: { ...testAppFragment },
},
provisioningProfile: {
id: 'test-provisioning-profile-id',
developerPortalIdentifier: 'test-developer-identifier',
},
},
],
};
export const testDistCert: DistributionCertificate = {
certP12: 'Y2VydHAxMg==',
certPassword: 'test-password',
@@ -0,0 +1,141 @@
import {
DistributionCertificate,
DistributionCertificateStoreInfo,
ProvisioningProfile,
ProvisioningProfileStoreInfo,
PushKey,
PushKeyStoreInfo,
} from '../ios/appstore/Credentials.types';
import { IosDistCredentials, IosPushCredentials } from '../ios/credentials';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
import { testBundleIdentifier, testExperienceName } from './fixtures-constants';
const today = new Date();
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
export const testAppleTeam = {
id: 'test-team-id',
};
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
teamId: 'id',
};
export const testProvisioningProfiles = [testProvisioningProfile];
export const testProvisioningProfileFromApple: ProvisioningProfileStoreInfo = {
name: 'test-name',
status: 'Active',
expires: tomorrow.getTime(),
distributionMethod: 'test',
certificates: [],
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
provisioningProfile: testProvisioningProfile.provisioningProfile,
teamId: 'id',
};
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];
export const testDistCert: DistributionCertificate = {
certP12: 'Y2VydHAxMg==',
certPassword: 'test-password',
distCertSerialNumber: 'test-serial',
teamId: 'test-team-id',
};
export const testIosDistCredential: IosDistCredentials = {
id: 1,
type: 'dist-cert',
...testDistCert,
};
export const testIosDistCredentials = [testIosDistCredential];
export const testDistCertFromApple: DistributionCertificateStoreInfo = {
id: 'test-id',
name: 'test-name',
status: 'Active',
created: today.getTime(),
expires: tomorrow.getTime(),
ownerName: 'test-owner',
ownerId: 'test-id',
serialNumber: testIosDistCredential.distCertSerialNumber as string,
};
export const testDistCertsFromApple = [testDistCertFromApple];
export const testPushKey: PushKey = {
apnsKeyP8: 'test-p8',
apnsKeyId: 'test-key-id',
teamId: 'test-team-id',
};
export const testIosPushCredential: IosPushCredentials = {
id: 2,
type: 'push-key',
...testPushKey,
};
export const testIosPushCredentials = [testIosPushCredential];
export const testPushKeyFromApple: PushKeyStoreInfo = {
id: testIosPushCredential.apnsKeyId,
name: 'test-name',
};
export const testPushKeysFromApple = [testPushKeyFromApple];
export const testLegacyPushCert = {
pushId: 'test-push-id',
pushP12: 'test-push-p12',
pushPassword: 'test-push-password',
};
export const testAppCredential = {
experienceName: testExperienceName,
bundleIdentifier: testBundleIdentifier,
distCredentialsId: testIosDistCredential.id,
pushCredentialsId: testIosPushCredential.id,
credentials: {
...testProvisioningProfile,
},
};
export const testAllCredentialsForApp = {
...testAppCredential,
pushCredentials: testPushKey,
distCredentials: testDistCert,
};
export const testAppCredentials = [testAppCredential];
export const testAllCredentials = {
userCredentials: [...testIosDistCredentials, ...testIosPushCredentials],
appCredentials: testAppCredentials,
};
export function getNewIosApiMockWithoutCredentials() {
return {
getAppAsync: jest.fn(),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingAppleTeamAsync: jest.fn(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
getDevicesForAppleTeamAsync: jest.fn(),
createProvisioningProfileAsync: jest.fn(),
getProvisioningProfileAsync: jest.fn(),
updateProvisioningProfileAsync: jest.fn(),
deleteProvisioningProfilesAsync: jest.fn(),
getDistributionCertificateForAppAsync: jest.fn(),
getDistributionCertificatesForAccountAsync: jest.fn(),
createDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
};
}
export function getNewIosApiMockWithCredentials() {
return {
getAppAsync: jest.fn(),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync: jest.fn(),
createOrGetExistingAppleTeamAsync: jest.fn(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(),
getDevicesForAppleTeamAsync: jest.fn(),
createProvisioningProfileAsync: jest.fn(),
getProvisioningProfileAsync: jest.fn(),
updateProvisioningProfileAsync: jest.fn(),
deleteProvisioningProfilesAsync: jest.fn(),
getDistributionCertificateForAppAsync: jest.fn(),
getDistributionCertificatesForAccountAsync: jest.fn(),
createDistributionCertificateAsync: jest.fn(),
deleteDistributionCertificateAsync: jest.fn(),
};
}
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import log from '../../log';
import Log from '../../log';
import { CredentialsManager } from '../CredentialsManager';
import { CredentialsProvider } from '../CredentialsProvider';
import { Context } from '../context';
@@ -46,7 +46,7 @@ export default class AndroidCredentialsProvider implements CredentialsProvider {
const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.ctx.projectDir);
return !!rawCredentialsJson?.android;
} catch (err) {
log.error(err); // malformed json
Log.error(err); // malformed json
return false;
}
}
@@ -1,7 +1,7 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import log from '../../../log';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { maybeRenameExistingFileAsync } from '../../../utils/files';
import { Action, CredentialsManager } from '../../CredentialsManager';
@@ -18,7 +18,7 @@ export class DownloadKeystore implements Action {
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
const keystore = await ctx.android.fetchKeystoreAsync(this.projectFullName);
if (!keystore) {
log.warn('There is no valid Keystore defined for this app');
Log.warn('There is no valid Keystore defined for this app');
return;
}
@@ -34,11 +34,11 @@ export class DownloadKeystore implements Action {
await maybeRenameExistingFileAsync(ctx.projectDir, keystorePath);
log(`Saving Keystore to ${keystorePath}`);
Log.log(`Saving Keystore to ${keystorePath}`);
await fs.writeFile(keystorePath, keystore.keystore, 'base64');
if (displayCredentials) {
log(`Keystore credentials
Log.log(`Keystore credentials
Keystore password: ${chalk.bold(keystore.keystorePassword)}
Key alias: ${chalk.bold(keystore.keyAlias)}
Key password: ${chalk.bold(keystore.keyPassword)}
@@ -54,7 +54,7 @@ export class BackupKeystore implements Action {
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
if (await ctx.android.fetchKeystoreAsync(this.projectFullName)) {
log.warn('Backing up your old Android Keystore now...');
Log.warn('Backing up your old Android Keystore now...');
await manager.runActionAsync(
new DownloadKeystore(this.projectFullName, {
displayCredentials: true,
@@ -1,4 +1,4 @@
import log from '../../../log';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -17,6 +17,6 @@ export class UpdateFcmKey implements Action {
]);
await ctx.android.updateFcmKeyAsync(this.experienceName, fcmApiKey);
log.succeed('Updated successfully');
Log.succeed('Updated successfully');
}
}
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import log from '../../../log';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -11,7 +11,7 @@ export class RemoveKeystore implements Action {
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
if (!(await ctx.android.fetchKeystoreAsync(this.projectFullName))) {
log.warn('There is no Keystore defined for this app.');
Log.warn('There is no Keystore defined for this app.');
return;
}
@@ -31,29 +31,29 @@ export class RemoveKeystore implements Action {
await manager.runActionAsync(new BackupKeystore(this.projectFullName));
await ctx.android.removeKeystoreAsync(this.projectFullName);
log.succeed('Keystore removed');
Log.succeed('Keystore removed');
}
}
displayWarning() {
log.newLine();
log.warn(
Log.newLine();
Log.warn(
`Clearing your Android build credentials from our build servers is a ${chalk.bold(
'PERMANENT and IRREVERSIBLE action.'
)}`
);
log.warn(
Log.warn(
chalk.bold(
'Android Keystore must be identical to the one previously used to submit your app to the Google Play Store.'
)
);
log.warn(
Log.warn(
'Please read https://docs.expo.io/distribution/building-standalone-apps/#if-you-choose-to-build-for-android for more info before proceeding.'
);
log.newLine();
log.warn(
Log.newLine();
Log.warn(
chalk.bold('Your Keystore will be backed up to your current directory if you continue.')
);
log.newLine();
Log.newLine();
}
}
@@ -1,4 +1,4 @@
import log from '../../../log';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { readAndroidCredentialsAsync } from '../../credentialsJson/read';
@@ -31,7 +31,7 @@ export class SetupBuildCredentialsFromCredentialsJson implements Action {
try {
localCredentials = await readAndroidCredentialsAsync(ctx.projectDir);
} catch (error) {
log.error(
Log.error(
'Reading credentials from credentials.json failed. Make sure this file is correct and all credentials are present there.'
);
throw error;
@@ -40,6 +40,6 @@ export class SetupBuildCredentialsFromCredentialsJson implements Action {
await validateKeystoreAsync(localCredentials.keystore);
}
await ctx.android.updateKeystoreAsync(this.projectFullName, localCredentials.keystore);
log.succeed('Keystore updated');
Log.succeed('Keystore updated');
}
}
@@ -1,4 +1,4 @@
import log from '../../../log';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { updateAndroidCredentialsAsync } from '../../credentialsJson/update';
@@ -7,9 +7,9 @@ export class UpdateCredentialsJson implements Action {
constructor(private projectFullName: string) {}
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
log('Updating Android credentials in credentials.json');
Log.log('Updating Android credentials in credentials.json');
await updateAndroidCredentialsAsync(ctx);
log.succeed(
Log.succeed(
'Android part of your local credentials.json is synced with values stored on EAS servers.'
);
}
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import log from '../../../log';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
@@ -17,7 +17,7 @@ export class UpdateKeystore implements Action {
const keystore = await this.provideOrGenerateAsync();
await ctx.android.updateKeystoreAsync(this.projectFullName, keystore);
log.succeed('Keystore updated');
Log.succeed('Keystore updated');
}
private async provideOrGenerateAsync(): Promise<Keystore> {
@@ -30,17 +30,17 @@ export class UpdateKeystore implements Action {
}
private displayWarning() {
log.newLine();
log.warn(
Log.newLine();
Log.warn(
`Updating your Android build credentials will remove previous version from our servers, this is a ${chalk.bold(
'PERMANENT and IRREVERSIBLE action.'
)}`
);
log.warn(
Log.warn(
chalk.bold(
'Android Keystore must be identical to the one previously used to submit your app to the Google Play Store.'
)
);
log.newLine();
Log.newLine();
}
}
@@ -5,7 +5,7 @@ import path from 'path';
import tempy from 'tempy';
import { v4 as uuidv4 } from 'uuid';
import log from '../../../log';
import Log from '../../../log';
import { getTmpDirectory } from '../../../utils/paths';
import { Keystore } from '../credentials';
@@ -20,9 +20,9 @@ export async function keytoolCommandExistsAsync(): Promise<boolean> {
async function ensureKeytoolCommandExistsAsync(): Promise<void> {
if (!(await keytoolCommandExistsAsync())) {
log.error('keytool is required to run this command, make sure you have it installed?');
log.warn('keytool is a part of OpenJDK: https://openjdk.java.net/');
log.warn('Also make sure that keytool is in your PATH after installation.');
Log.error('keytool is required to run this command, make sure you have it installed?');
Log.warn('keytool is a part of OpenJDK: https://openjdk.java.net/');
Log.warn('Also make sure that keytool is in your PATH after installation.');
throw new Error('keytool not found');
}
}
@@ -70,12 +70,12 @@ export async function logKeystoreHashesAsync(keystore: Keystore, linePrefix: str
const googleHash = crypto.createHash('sha1').update(data).digest('hex').toUpperCase();
const googleHash256 = crypto.createHash('sha256').update(data).digest('hex').toUpperCase();
const fbHash = crypto.createHash('sha1').update(data).digest('base64');
log(
Log.log(
`${linePrefix}Google Certificate Fingerprint: ${googleHash.replace(/(.{2}(?!$))/g, '$1:')}`
);
log(`${linePrefix}Google Certificate Hash (SHA-1): ${googleHash}`);
log(`${linePrefix}Google Certificate Hash (SHA-256): ${googleHash256}`);
log(`${linePrefix}Facebook Key Hash: ${fbHash}`);
Log.log(`${linePrefix}Google Certificate Hash (SHA-1): ${googleHash}`);
Log.log(`${linePrefix}Google Certificate Hash (SHA-256): ${googleHash256}`);
Log.log(`${linePrefix}Facebook Key Hash: ${fbHash}`);
}
async function createKeystoreAsync(credentials: {
@@ -1,34 +1,34 @@
import chalk from 'chalk';
import log from '../../../log';
import Log from '../../../log';
import { AndroidCredentials } from '../credentials';
import { keytoolCommandExistsAsync, logKeystoreHashesAsync } from '../utils/keystore';
export async function printAndroidCredentials(credentialsList: AndroidCredentials[]) {
log(chalk.bold('Available Android credentials'));
log.newLine();
Log.log(chalk.bold('Available Android credentials'));
Log.newLine();
for (const credentials of credentialsList) {
await printAndroidAppCredentials(credentials);
}
}
export async function printAndroidAppCredentials(credentials: AndroidCredentials) {
log(chalk.green(credentials.experienceName));
log(chalk.bold(' Keystore hashes'));
Log.log(chalk.green(credentials.experienceName));
Log.log(chalk.bold(' Keystore hashes'));
if (credentials.keystore?.keystore) {
if (await keytoolCommandExistsAsync()) {
try {
await logKeystoreHashesAsync(credentials.keystore, ' ');
} catch (error) {
log.error(' Failed to parse the Keystore', error);
Log.error(' Failed to parse the Keystore', error);
}
} else {
log.warn(' keytool is required to calulate Keystore hashes');
Log.warn(' keytool is required to calulate Keystore hashes');
}
} else {
log(' -----------------------');
Log.log(' -----------------------');
}
log(chalk.bold(' Push Notifications credentials'));
log(' FCM API Key: ', credentials.pushCredentials?.fcmApiKey ?? '---------------------');
log('\n');
Log.log(chalk.bold(' Push Notifications credentials'));
Log.log(' FCM API Key: ', credentials.pushCredentials?.fcmApiKey ?? '---------------------');
Log.log('\n');
}
+6 -6
View File
@@ -2,7 +2,7 @@ import { ExpoConfig, getConfig } from '@expo/config';
import chalk from 'chalk';
import pick from 'lodash/pick';
import log from '../log';
import Log from '../log';
import { getProjectAccountName } from '../project/projectUtils';
import { confirmAsync } from '../prompts';
import { Actor } from '../user/User';
@@ -95,13 +95,13 @@ class CredentialsContext implements Context {
// Figure out if User A is configuring credentials as admin for User B's project
const isProxyUser = this.user.__typename === 'Robot' || owner !== this.user.username;
log(
Log.log(
`Accessing credentials ${isProxyUser ? 'on behalf of' : 'for'} ${owner} in project ${
this.exp.slug
}`
);
} else {
log(`Accessing credentials for ${this.exp.owner ?? getActorDisplayName(this.user)}`);
Log.log(`Accessing credentials for ${this.exp.owner ?? getActorDisplayName(this.user)}`);
}
}
@@ -115,12 +115,12 @@ class CredentialsContext implements Context {
return;
}
log(
Log.log(
chalk.green(
'If you provide your Apple account credentials we will be able to generate all necessary build credentials and fully validate them.'
)
);
log(
Log.log(
chalk.green(
'This is optional, but without Apple account access you will need to provide all the values manually and we can only run minimal validation on them.'
)
@@ -131,7 +131,7 @@ class CredentialsContext implements Context {
if (confirm) {
await this.appStore.ensureAuthenticatedAsync();
} else {
log(
Log.log(
chalk.green(
'No problem! 👌 If you select an action that requires those credentials we will ask you again about it.'
)
@@ -1,7 +1,7 @@
import fs from 'fs-extra';
import path from 'path';
import log from '../../log';
import Log from '../../log';
import { getProjectAccountName } from '../../project/projectUtils';
import { confirmAsync } from '../../prompts';
import { gitStatusAsync } from '../../utils/git';
@@ -21,8 +21,8 @@ export async function updateAndroidCredentialsAsync(ctx: Context): Promise<void>
const rawFile = await fs.readFile(credentialsJsonFilePath, 'utf-8');
rawCredentialsJsonObject = JSON.parse(rawFile);
} catch (error) {
log.error(`Reading credentials.json failed [${error}]`);
log.error('Make sure that file is correct (or remove it) and rerun this command.');
Log.error(`Reading credentials.json failed [${error}]`);
Log.error('Make sure that file is correct (or remove it) and rerun this command.');
throw error;
}
}
@@ -42,14 +42,14 @@ export async function updateAndroidCredentialsAsync(ctx: Context): Promise<void>
'Credentials on EAS servers might be invalid or incomplete. Are you sure you want to continue?',
});
if (!confirm) {
log.warn('Aborting...');
Log.warn('Aborting...');
return;
}
}
const keystorePath =
rawCredentialsJsonObject?.android?.keystore?.keystorePath ?? 'android/keystores/keystore.jks';
log(`Writing Keystore to ${keystorePath}`);
Log.log(`Writing Keystore to ${keystorePath}`);
await updateFileAsync(ctx.projectDir, keystorePath, keystore.keystore);
const shouldWarnKeystore = await isFileUntrackedAsync(keystorePath);
@@ -94,8 +94,8 @@ export async function updateIosCredentialsAsync(
const rawFile = await fs.readFile(credentialsJsonFilePath, 'utf-8');
rawCredentialsJsonObject = JSON.parse(rawFile);
} catch (error) {
log.error(`There was an error while reading credentials.json [${error}]`);
log.error('Make sure that file is correct (or remove it) and rerun this command.');
Log.error(`There was an error while reading credentials.json [${error}]`);
Log.error('Make sure that file is correct (or remove it) and rerun this command.');
throw error;
}
@@ -106,11 +106,11 @@ export async function updateIosCredentialsAsync(
key => key !== 'provisioningProfilePath' && key !== 'distributionCertificate'
);
if (maybeUnknownKey) {
log.error(
Log.error(
`It looks like your credentials.json either contains multi-target credentials or you've made a typo.`
);
log.error(`- The key in 'ios' object that EAS CLI encountered is: ${maybeUnknownKey}`);
log.error(
Log.error(`- The key in 'ios' object that EAS CLI encountered is: ${maybeUnknownKey}`);
Log.error(
`- Updating credentials.json (for multi-target iOS projects) with credentials stored on EAS servers is not supported at the moment, sorry!`
);
throw new Error('Updating credentials.json failed');
@@ -145,12 +145,12 @@ export async function updateIosCredentialsAsync(
'Credentials on EAS servers might be invalid or incomplete. Are you sure you want to continue?',
});
if (!confirm) {
log.warn('Aborting...');
Log.warn('Aborting...');
return;
}
}
log(`Writing Provisioning Profile to ${profilePath}`);
Log.log(`Writing Provisioning Profile to ${profilePath}`);
await updateFileAsync(
ctx.projectDir,
profilePath,
@@ -158,7 +158,7 @@ export async function updateIosCredentialsAsync(
);
const shouldWarnPProfile = await isFileUntrackedAsync(profilePath);
log(`Writing Distribution Certificate to ${distCertPath}`);
Log.log(`Writing Distribution Certificate to ${distCertPath}`);
await updateFileAsync(ctx.projectDir, distCertPath, distCredentials?.certP12);
const shouldWarnDistCert = await isFileUntrackedAsync(distCertPath);
@@ -217,11 +217,11 @@ async function isFileUntrackedAsync(path: string): Promise<boolean> {
function displayUntrackedFilesWarning(newFilePaths: string[]) {
if (newFilePaths.length === 1) {
log.warn(
Log.warn(
`File ${newFilePaths[0]} is currently untracked, remember to add it to .gitignore or to encrypt it. (e.g. with git-crypt)`
);
} else if (newFilePaths.length > 1) {
log.warn(
Log.warn(
`Files ${newFilePaths.join(
', '
)} are currently untracked, remember to add them to .gitignore or to encrypt them. (e.g. with git-crypt)`
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, DistributionType } from '@expo/eas-json';
import { IosDistributionType } from '../../graphql/generated';
import log from '../../log';
import Log from '../../log';
import { findAccountByName } from '../../user/Account';
import { CredentialsManager } from '../CredentialsManager';
import { CredentialsProvider } from '../CredentialsProvider';
@@ -59,7 +59,7 @@ export default class IosCredentialsProvider implements CredentialsProvider {
const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.ctx.projectDir);
return !!rawCredentialsJson?.ios;
} catch (err) {
log.error(err); // malformed json
Log.error(err); // malformed json
return false;
}
}
@@ -111,7 +111,7 @@ export default class IosCredentialsProvider implements CredentialsProvider {
private async getRemoteAsync(): Promise<IosCredentials> {
if (this.options.skipCredentialsCheck) {
log('Skipping credentials check');
Log.log('Skipping credentials check');
} else {
await new CredentialsManager(this.ctx).runActionAsync(
new SetupBuildCredentials(this.options.app, this.options.distribution)
@@ -2,7 +2,7 @@ import assert from 'assert';
import chalk from 'chalk';
import ora from 'ora';
import log from '../../../log';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import {
@@ -36,14 +36,14 @@ export class ConfigureProvisioningProfile implements Action {
}
if (!profile.provisioningProfileId) {
log.warn("The provisioning profile we have on file cannot be validated on Apple's servers.");
Log.warn("The provisioning profile we have on file cannot be validated on Apple's servers.");
return;
}
if (ctx.appStore.authCtx) {
const distCert = await ctx.ios.getDistributionCertificateAsync(this.app);
if (!distCert) {
log.warn('There is no distribution certificate for this app.');
Log.warn('There is no distribution certificate for this app.');
return;
}
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(
@@ -53,15 +53,15 @@ export class ConfigureProvisioningProfile implements Action {
appleInfo => appleInfo.provisioningProfileId === profile.provisioningProfileId
);
if (!profilesWithMatchingId || profilesWithMatchingId.length < 1) {
log.warn('This profile is no longer valid on Apple Developer Portal.');
Log.warn('This profile is no longer valid on Apple Developer Portal.');
}
await this.configureAndUpdateAsync(ctx, this.app, distCert, profilesWithMatchingId[0]);
} else {
log.warn(
Log.warn(
"Without access to your Apple account we can't configure provisioning profiles for you."
);
log.warn('Make sure to recreate the profile if you selected a new distribution certificate.');
Log.warn('Make sure to recreate the profile if you selected a new distribution certificate.');
}
}
@@ -1,6 +1,6 @@
import assert from 'assert';
import log from '../../../log';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { IosDistCredentials } from '../credentials';
@@ -14,9 +14,9 @@ export class CreateDistributionCertificateStandaloneManager implements Action {
const action = new CreateDistributionCertificate(this.accountName);
await manager.runActionAsync(action);
log.newLine();
Log.newLine();
displayIosUserCredentials(action.distCredentials);
log.newLine();
Log.newLine();
}
}
@@ -40,6 +40,6 @@ export class CreateDistributionCertificate implements Action {
this.accountName,
distCert
);
log.succeed('Created distribution certificate');
Log.succeed('Created distribution certificate');
}
}
@@ -1,6 +1,6 @@
import assert from 'assert';
import log from '../../../log';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
@@ -17,7 +17,7 @@ export class CreateProvisioningProfileStandaloneManager implements Action {
const appCredentials = await ctx.ios.getAppCredentialsAsync(this.app);
displayIosAppCredentials(appCredentials);
log.newLine();
Log.newLine();
}
}
@@ -28,14 +28,14 @@ export class CreateProvisioningProfile implements Action {
const provisioningProfile = await this.provideOrGenerateAsync(ctx);
await ctx.ios.updateProvisioningProfileAsync(this.app, provisioningProfile);
log.succeed('Created provisioning profile');
Log.succeed('Created provisioning profile');
}
private async provideOrGenerateAsync(ctx: Context): Promise<ProvisioningProfile> {
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
if (userProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
log.warn('Provisioning profile: Unable to validate specified profile.');
Log.warn('Provisioning profile: Unable to validate specified profile.');
return userProvided;
}
const distCert = await ctx.ios.getDistributionCertificateAsync(this.app);
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import log, { learnMore } from '../../../log';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -39,14 +39,14 @@ export async function provideOrGenerateDistributionCertificateAsync(
const userProvided = await promptForDistCertAsync(ctx);
if (userProvided) {
if (!ctx.appStore.authCtx) {
log.warn(
Log.warn(
'Unable to validate distribution certificate due to insufficient Apple Credentials'
);
return userProvided;
} else {
const isValid = await validateDistributionCertificateAsync(ctx, userProvided);
if (!isValid) {
log.warn("Provided Distribution Certificate is no longer valid on Apple's server");
Log.warn("Provided Distribution Certificate is no longer valid on Apple's server");
}
return isValid
? userProvided
@@ -88,8 +88,8 @@ async function generateDistributionCertificateAsync(
} catch (e) {
if (e instanceof AppleTooManyCertsError) {
const distCerts = await ctx.appStore.listDistributionCertificatesAsync();
log.warn('Maximum number of Distribution Certificates generated on Apple Developer Portal.');
log.warn(APPLE_DIST_CERTS_TOO_MANY_GENERATED_ERROR);
Log.warn('Maximum number of Distribution Certificates generated on Apple Developer Portal.');
Log.warn(APPLE_DIST_CERTS_TOO_MANY_GENERATED_ERROR);
if (ctx.nonInteractive) {
throw new Error(
@@ -97,13 +97,13 @@ async function generateDistributionCertificateAsync(
);
}
log(
Log.log(
chalk.grey(
`✅ Distribution Certificates can be revoked with no side effects for App Store builds.`
)
);
log(learnMore('https://docs.expo.io/distribution/app-signing/#summary'));
log.newLine();
Log.log(learnMore('https://docs.expo.io/distribution/app-signing/#summary'));
Log.newLine();
const { distCertsToRevoke } = await promptAsync({
type: 'multiselect',
@@ -151,7 +151,7 @@ export async function selectDistributionCertificateAsync(
options.filterInvalid && validDistCredentials ? validDistCredentials : distCredentials;
if (distCredentials.length === 0) {
log.warn('There are no Distribution Certificates available in your Expo account.');
Log.warn('There are no Distribution Certificates available in your Expo account.');
return null;
}
@@ -241,7 +241,7 @@ export async function getValidDistCertsAsync(
(cred): cred is IosDistCredentials => cred.type === 'dist-cert'
);
if (!ctx.appStore.authCtx) {
log(chalk.yellow(`Unable to determine validity of Distribution Certificates.`));
Log.log(chalk.yellow(`Unable to determine validity of Distribution Certificates.`));
return distCredentials;
}
const certInfoFromApple = await ctx.appStore.listDistributionCertificatesAsync();
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import log from '../../../log';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Context } from '../../context';
import {
@@ -16,7 +16,7 @@ export async function selectProvisioningProfileFromAppleAsync(
): Promise<ProvisioningProfileStoreInfo | null> {
const profiles = await ctx.appStore.listProvisioningProfilesAsync(bundleIdentifier);
if (profiles.length === 0) {
log.warn(
Log.warn(
`There are no provisioning profiles available on Apple Developer Portal for bundle identifier ${bundleIdentifier}.`
);
return null;
@@ -53,7 +53,7 @@ export async function selectProvisioningProfileFromExpoAsync(
({ credentials }) => !!credentials.provisioningProfile && !!credentials.provisioningProfileId
);
if (profiles.length === 0) {
log.warn('There are no provisioning profiles available in your Expo account.');
Log.warn('There are no provisioning profiles available in your Expo account.');
return null;
}
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import log from '../../../log';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -21,8 +21,8 @@ export class RemoveDistributionCertificate implements Action {
await manager.runActionAsync(
new RemoveSpecificDistributionCertificate(selected?.id, this.accountName, this.options)
);
log.succeed('Removed distribution certificate');
log.newLine();
Log.succeed('Removed distribution certificate');
Log.newLine();
}
}
}
@@ -54,12 +54,12 @@ export class RemoveSpecificDistributionCertificate implements Action {
message: `You are removing certificate used by ${appList}. Do you want to continue?`,
});
if (!confirm) {
log('Aborting');
Log.log('Aborting');
return;
}
}
log('Removing Distribution Certificate');
Log.log('Removing Distribution Certificate');
await ctx.ios.deleteDistributionCertificateAsync(this.userCredentialsId, this.accountName);
let shouldRevoke = this.options.shouldRevoke;
@@ -91,7 +91,7 @@ export class RemoveSpecificDistributionCertificate implements Action {
if (!(await ctx.ios.getProvisioningProfileAsync(appLookupParams))) {
continue;
}
log(
Log.log(
`Removing Provisioning Profile for ${appCredentials.experienceName} (${appCredentials.bundleIdentifier})`
);
await manager.runActionAsync(
@@ -1,4 +1,4 @@
import log from '../../../log';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -30,7 +30,7 @@ export class RemoveSpecificProvisioningProfile implements Action {
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
await ctx.ios.deleteProvisioningProfileAsync(this.app);
log.succeed(
Log.succeed(
`Removed provisioning profile for @${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`
);
@@ -3,7 +3,7 @@ import assert from 'assert';
import chalk from 'chalk';
import { AppleDevice, IosAppBuildCredentialsFragment } from '../../../graphql/generated';
import log from '../../../log';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { findAccountByName } from '../../../user/Account';
import { Action, CredentialsManager } from '../../CredentialsManager';
@@ -53,7 +53,7 @@ export class SetupBuildCredentials implements Action {
await manager.runActionAsync(new SetupProvisioningProfile(this.app));
}
} catch (error) {
log.error('Failed to setup credentials.');
Log.error('Failed to setup credentials.');
throw error;
}
@@ -63,9 +63,9 @@ export class SetupBuildCredentials implements Action {
);
const appInfo = `@${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`;
displayProjectCredentials(this.app, appCredentials, /* pushKey */ null, distCert);
log.newLine();
log(chalk.green(`All credentials are ready to build ${appInfo}`));
log.newLine();
Log.newLine();
Log.log(chalk.green(`All credentials are ready to build ${appInfo}`));
Log.newLine();
}
async unifyCredentialsFormatAsync(
@@ -121,7 +121,7 @@ export class SetupBuildCredentialsFromCredentialsJson implements Action {
try {
localCredentials = await readIosCredentialsAsync(ctx.projectDir);
} catch (error) {
log.error(
Log.error(
'Reading credentials from credentials.json failed. Make sure this file is correct and all credentials are present there.'
);
throw error;
@@ -198,8 +198,8 @@ export class SetupBuildCredentialsFromCredentialsJson implements Action {
undefined,
await ctx.ios.getDistributionCertificateAsync(this.app)
);
log.newLine();
log(chalk.green(`All credentials are ready to build ${appInfo}`));
log.newLine();
Log.newLine();
Log.log(chalk.green(`All credentials are ready to build ${appInfo}`));
Log.newLine();
}
}
@@ -1,4 +1,4 @@
import log from '../../../log';
import Log from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -54,7 +54,7 @@ export class SetupDistributionCertificateForApp implements Action {
return;
}
log(`Using Distribution Certificate: ${autoselectedCertificate.certId || '-----'}`);
Log.log(`Using Distribution Certificate: ${autoselectedCertificate.certId || '-----'}`);
await manager.runActionAsync(
new UseSpecificDistributionCertificate(this.app, autoselectedCertificate.id)
);
@@ -68,7 +68,7 @@ export class SetupDistributionCertificateForApp implements Action {
if (ctx.appStore.authCtx) {
const isValid = await validateDistributionCertificateAsync(ctx, currentCertificate);
if (!isValid) {
log.warn("Current Distribution Certificate is no longer valid on Apple's server");
Log.warn("Current Distribution Certificate is no longer valid on Apple's server");
}
return isValid;
}
@@ -1,7 +1,7 @@
import assert from 'assert';
import pick from 'lodash/pick';
import log from '../../../log';
import Log from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -65,7 +65,7 @@ export class SetupProvisioningProfile implements Action {
return;
}
} else {
log(`Using provisioning profile: ${autoselectedProfile.provisioningProfileId}`);
Log.log(`Using provisioning profile: ${autoselectedProfile.provisioningProfileId}`);
}
await ctx.ios.updateProvisioningProfileAsync(
@@ -88,7 +88,7 @@ export class SetupProvisioningProfile implements Action {
}
if (!ctx.appStore.authCtx || !currentProfile.provisioningProfileId) {
if (!currentProfile.provisioningProfileId) {
log.warn(
Log.warn(
"The provisioning profile we have on file cannot be validated on Apple's servers."
);
}
@@ -98,7 +98,7 @@ export class SetupProvisioningProfile implements Action {
this.app.bundleIdentifier
);
if (!validationResult.ok) {
log.warn(validationResult.error);
Log.warn(validationResult.error);
return false;
}
return true;
@@ -110,7 +110,7 @@ export class SetupProvisioningProfile implements Action {
this.app.bundleIdentifier
);
if (!validationResult.ok) {
log.warn(validationResult.error);
Log.warn(validationResult.error);
return false;
}
return true;
@@ -1,6 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import log from '../../../log';
import Log from '../../../log';
import { ensureAppIdentifierIsDefinedAsync } from '../../../project/projectUtils';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -12,9 +12,9 @@ export class UpdateCredentialsJson implements Action {
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
const bundleIdentifer = await ensureAppIdentifierIsDefinedAsync(ctx.projectDir, Platform.iOS);
log('Updating iOS credentials in credentials.json');
Log.log('Updating iOS credentials in credentials.json');
await updateIosCredentialsAsync(ctx, bundleIdentifer);
log.succeed(
Log.succeed(
'iOS part of your local credentials.json is synced with values stored on EAS servers.'
);
}
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import log from '../../../log';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
@@ -35,7 +35,7 @@ export class UpdateDistributionCertificate implements Action {
message: `You are updating certificate used by ${appList}. Do you want to continue?`,
});
if (!confirm) {
log('Aborting update process');
Log.log('Aborting update process');
return;
}
}
@@ -49,7 +49,7 @@ export class UpdateDistributionCertificate implements Action {
this.accountName
);
displayIosUserCredentials(newDistCert);
log.newLine();
Log.newLine();
}
}
@@ -72,11 +72,11 @@ export class UpdateSpecificDistributionCertificate implements Action {
this.accountName,
newDistCert
);
log.succeed('Updated distribution certificate');
log.newLine();
Log.succeed('Updated distribution certificate');
Log.newLine();
for (const appCredentials of apps) {
log(
Log.log(
`Removing provisioning profile for ${appCredentials.experienceName} (${appCredentials.bundleIdentifier})`
);
const appLookupParams = getAppLookupParams(
@@ -1,4 +1,4 @@
import log from '../../../log';
import Log from '../../../log';
import { Action, CredentialsManager } from '../../CredentialsManager';
import { Context } from '../../context';
import { AppLookupParams } from '../credentials';
@@ -27,7 +27,7 @@ export class UseSpecificDistributionCertificate implements Action {
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
await ctx.ios.useDistributionCertificateAsync(this.app, this.userCredentialsId);
log.succeed(
Log.succeed(
`Assigned distribution certificate to @${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`
);
}
@@ -1,6 +1,6 @@
import assert from 'assert';
import log from '../../../../log';
import Log from '../../../../log';
import { Action, CredentialsManager } from '../../../CredentialsManager';
import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
@@ -30,6 +30,6 @@ export class CreateDistributionCertificate implements Action {
this.app,
distCert
);
log.succeed('Created distribution certificate');
Log.succeed('Created distribution certificate');
}
}
@@ -1,23 +1,142 @@
import { AppleTeamFragment } from '../../../../graphql/generated';
import { AppleDistributionCertificateQueryResult } from '../../api/graphql/queries/AppleDistributionCertificateQuery';
import assert from 'assert';
import chalk from 'chalk';
import dateformat from 'dateformat';
export function formatDistributionCertificate({
developerPortalIdentifier,
serialNumber,
appleTeam,
validityNotBefore,
validityNotAfter,
}: AppleDistributionCertificateQueryResult): string {
import {
AppleDistributionCertificateFragment,
AppleTeamFragment,
} from '../../../../graphql/generated';
import Log from '../../../../log';
import { promptAsync } from '../../../../prompts';
import { Account } from '../../../../user/Account';
import { fromNow } from '../../../../utils/date';
import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { filterRevokedDistributionCerts } from '../../appstore/CredentialsUtilsBeta';
export function formatDistributionCertificate(
distributionCertificate: AppleDistributionCertificateFragment,
validSerialNumbers?: string[]
): string {
const {
serialNumber,
developerPortalIdentifier,
appleTeam,
validityNotBefore,
validityNotAfter,
updatedAt,
} = distributionCertificate;
let line: string = '';
if (developerPortalIdentifier) {
line += `Cert ID: ${developerPortalIdentifier}`;
}
line += `${line === '' ? '' : ', '}Serial number: ${serialNumber}${
appleTeam ? `, ${formatAppleTeam(appleTeam)}` : ''
}, Created: ${validityNotBefore}, Expires: ${validityNotAfter}`;
}`;
line += chalk.gray(
`\n Created: ${fromNow(new Date(validityNotBefore))} ago, Updated: ${fromNow(
new Date(updatedAt)
)} ago,`
);
line += chalk.gray(`\n Expires: ${dateformat(validityNotAfter, 'expiresHeaderFormat')}`);
const apps = distributionCertificate.iosAppBuildCredentialsList.map(
buildCredentials => buildCredentials.iosAppCredentials.app
);
if (apps.length) {
const appFullNames = apps.map(app => app.fullName).join(',');
line += chalk.gray(`\n 📲 Used by: ${appFullNames}`);
}
if (validSerialNumbers?.includes(serialNumber)) {
line += chalk.gray("\n ✅ Currently valid on Apple's servers.");
} else {
line += '';
}
return line;
}
function formatAppleTeam({ appleTeamIdentifier, appleTeamName }: AppleTeamFragment): string {
return `Team ID: ${appleTeamIdentifier}${appleTeamName ? `, Team name: ${appleTeamName}` : ''}`;
}
async function _selectDistributionCertificateAsync(
distCerts: AppleDistributionCertificateFragment[],
validDistributionCertificates?: AppleDistributionCertificateFragment[]
): Promise<AppleDistributionCertificateFragment | null> {
const validDistCertSerialNumbers = validDistributionCertificates?.map(
distCert => distCert.serialNumber
);
const { chosenDistCert } = await promptAsync({
type: 'select',
name: 'chosenDistCert',
message: 'Select certificate from the list.',
choices: distCerts.map(distCert => ({
title: formatDistributionCertificate(distCert, validDistCertSerialNumbers),
value: distCert,
})),
});
return chosenDistCert;
}
/**
* select a distribution certificate from an account (validity status shown on a best effort basis)
* */
export async function selectDistributionCertificateWithDependenciesAsync(
ctx: Context,
account: Account
): Promise<AppleDistributionCertificateFragment | null> {
const distCertsForAccount = await ctx.newIos.getDistributionCertificatesForAccountAsync(account);
if (distCertsForAccount.length === 0) {
Log.warn(`There are no Distribution Certificates available in your EAS account.`);
return null;
}
if (!ctx.appStore.authCtx) {
return _selectDistributionCertificateAsync(distCertsForAccount);
}
// get valid certs on the developer portal
const certInfoFromApple = await ctx.appStore.listDistributionCertificatesAsync();
const validDistCerts = await filterRevokedDistributionCerts(
distCertsForAccount,
certInfoFromApple
);
return _selectDistributionCertificateAsync(distCertsForAccount, validDistCerts);
}
/**
* select a distribution certificate from a valid set (curated on a best effort basis)
* */
export async function selectValidDistributionCertificateAsync(
ctx: Context,
appLookupParams: AppLookupParams
): Promise<AppleDistributionCertificateFragment | null> {
const distCertsForAccount = await ctx.newIos.getDistributionCertificatesForAccountAsync(
appLookupParams.account
);
if (distCertsForAccount.length === 0) {
Log.warn(`There are no Distribution Certificates available in your EAS account.`);
return null;
}
if (!ctx.appStore.authCtx) {
return _selectDistributionCertificateAsync(distCertsForAccount);
}
// filter by apple team
assert(ctx.appStore.authCtx, 'authentication to the Apple App Store is required');
const appleTeamIdentifier = ctx.appStore.authCtx.team.id;
const distCertsForAppleTeam = distCertsForAccount.filter(distCert => {
return !distCert.appleTeam || distCert.appleTeam.appleTeamIdentifier === appleTeamIdentifier;
});
// filter by valid certs on the developer portal
const certInfoFromApple = await ctx.appStore.listDistributionCertificatesAsync();
const validDistCerts = await filterRevokedDistributionCerts(
distCertsForAppleTeam,
certInfoFromApple
);
Log.log(
`${validDistCerts.length}/${distCertsForAccount.length} Distribution Certificates are currently valid for Apple Team ${ctx.appStore.authCtx?.team.id}.`
);
return _selectDistributionCertificateAsync(validDistCerts);
}
@@ -1,13 +1,13 @@
import isEqual from 'lodash/isEqual';
import log from '../../../../log';
import { AppleDistributionCertificateFragment } from '../../../../graphql/generated';
import Log from '../../../../log';
import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/AppleDeviceQuery';
import { AppleDistributionCertificateQueryResult } from '../../api/graphql/queries/AppleDistributionCertificateQuery';
import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types';
export function isDevPortalAdhocProfileValid(
profileFromDevPortal: ProvisioningProfileStoreInfo | null,
distCert: AppleDistributionCertificateQueryResult,
distCert: AppleDistributionCertificateFragment,
expectedDevices: AppleDeviceFragmentWithAppleTeam[]
): boolean {
if (!profileFromDevPortal) {
@@ -15,7 +15,7 @@ export function isDevPortalAdhocProfileValid(
}
if (profileFromDevPortal.status === 'Invalid' || profileFromDevPortal.certificates.length === 0) {
log('Provisioning Profile is invalid');
Log.log('Provisioning Profile is invalid');
return false;
}
@@ -24,7 +24,7 @@ export function isDevPortalAdhocProfileValid(
distCert.developerPortalIdentifier &&
distCert.developerPortalIdentifier !== currentDistCertId
) {
log(
Log.log(
`Provisioning Profile was generated for Distribution Certificate with ID "${currentDistCertId}", expected ID "${distCert.developerPortalIdentifier}"`
);
return false;
@@ -33,7 +33,7 @@ export function isDevPortalAdhocProfileValid(
const profileDeviceUdids = (profileFromDevPortal.devices ?? []).map(({ udid }) => udid);
const expectedDeviceUdids = expectedDevices.map(({ identifier }) => identifier);
if (!doUDIDsMatch(profileDeviceUdids, expectedDeviceUdids)) {
log('Provisioning Profile was generated for another set of devices');
Log.log('Provisioning Profile was generated for another set of devices');
return false;
}
@@ -0,0 +1,91 @@
import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileIdentifiersFragment,
} from '../../../../graphql/generated';
import Log from '../../../../log';
import { confirmAsync } from '../../../../prompts';
import { Account } from '../../../../user/Account';
import { Action, CredentialsManager } from '../../../CredentialsManager';
import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { selectDistributionCertificateWithDependenciesAsync } from './DistributionCertificateUtils';
import { RemoveProvisioningProfiles } from './RemoveProvisioningProfile';
export class SelectAndRemoveDistributionCertificate implements Action {
constructor(private account: Account) {}
async runAsync(_manager: CredentialsManager, ctx: Context): Promise<void> {
const selected = await selectDistributionCertificateWithDependenciesAsync(ctx, this.account);
if (selected) {
await new RemoveDistributionCertificate(this.account, selected).runAsync(ctx);
Log.succeed('Removed distribution certificate');
Log.newLine();
}
}
}
export class RemoveDistributionCertificate {
constructor(
private account: Account,
private distributionCertificate: AppleDistributionCertificateFragment
) {}
public async runAsync(ctx: Context): Promise<void> {
const apps = this.distributionCertificate.iosAppBuildCredentialsList.map(
buildCredentials => buildCredentials.iosAppCredentials.app
);
if (apps.length !== 0) {
const appFullNames = apps.map(app => app.fullName).join(',');
if (ctx.nonInteractive) {
throw new Error(
`Certificate is currently used by ${appFullNames} and cannot be deleted in non-interactive mode.`
);
}
const confirm = await confirmAsync({
message: `You are removing certificate used by ${appFullNames}. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
return;
}
}
Log.log('Removing Distribution Certificate');
await ctx.newIos.deleteDistributionCertificateAsync(this.distributionCertificate.id);
if (this.distributionCertificate.developerPortalIdentifier) {
let shouldRevoke = false;
if (!ctx.nonInteractive) {
shouldRevoke = await confirmAsync({
message: `Do you also want to revoke this Distribution Certificate on Apple Developer Portal?`,
});
} else if (ctx.nonInteractive) {
Log.log('Skipping certificate revocation on the Apple Developer Portal.');
}
if (shouldRevoke) {
await ctx.appStore.revokeDistributionCertificateAsync([
this.distributionCertificate.developerPortalIdentifier,
]);
}
}
await this.removeInvalidProvisioningProfilesAsync(ctx);
}
private async removeInvalidProvisioningProfilesAsync(ctx: Context): Promise<void> {
const buildCredentialsList = this.distributionCertificate.iosAppBuildCredentialsList;
const appsWithProfilesToRemove: AppLookupParams[] = [];
const profilesToRemove: AppleProvisioningProfileIdentifiersFragment[] = [];
for (const buildCredentials of buildCredentialsList) {
const projectName = buildCredentials.iosAppCredentials.app.slug;
const { bundleIdentifier } = buildCredentials.iosAppCredentials.appleAppIdentifier;
const appLookupParams = { account: this.account, projectName, bundleIdentifier };
const maybeProvisioningProfile = buildCredentials.provisioningProfile;
if (maybeProvisioningProfile) {
appsWithProfilesToRemove.push(appLookupParams);
profilesToRemove.push(maybeProvisioningProfile);
}
}
await new RemoveProvisioningProfiles(appsWithProfilesToRemove, profilesToRemove).runAsync(ctx);
}
}
@@ -0,0 +1,32 @@
import { assert } from '@expo/config';
import { AppleProvisioningProfileIdentifiersFragment } from '../../../../graphql/generated';
import Log from '../../../../log';
import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
export class RemoveProvisioningProfiles {
constructor(
private apps: AppLookupParams[],
private provisioningProfiles: AppleProvisioningProfileIdentifiersFragment[]
) {
assert(
apps.length === provisioningProfiles.length,
'apps must correspond to the provisioning profiles being removed'
);
}
async runAsync(ctx: Context): Promise<void> {
if (this.provisioningProfiles.length === 0) {
Log.log(`Skipping deletion of Provisioning Profiles`);
return;
}
await ctx.newIos.deleteProvisioningProfilesAsync(
this.provisioningProfiles.map(profile => profile.id)
);
const appAndBundles = this.apps
.map(app => `@${app.account.name}/${app.projectName} (${app.bundleIdentifier})`)
.join(',');
Log.succeed(`Successfully removed provisioning profiles for ${appAndBundles}`);
}
}
@@ -1,25 +1,28 @@
import assert from 'assert';
import sortBy from 'lodash/sortBy';
import { AppleDistributionCertificate, IosDistributionType } from '../../../../graphql/generated';
import log from '../../../../log';
import {
AppleDistributionCertificate,
AppleDistributionCertificateFragment,
IosDistributionType,
} from '../../../../graphql/generated';
import Log from '../../../../log';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { Action, CredentialsManager } from '../../../CredentialsManager';
import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation';
import { AppleDistributionCertificateQueryResult } from '../../api/graphql/queries/AppleDistributionCertificateQuery';
import { getValidCertSerialNumbers } from '../../appstore/CredentialsUtils';
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
import { formatDistributionCertificate } from './DistributionCertificateUtils';
export class SetupDistributionCertificate implements Action {
private validDistCerts?: AppleDistributionCertificateQueryResult[];
private _distributionCertificate?: AppleDistributionCertificateQueryResult;
private validDistCerts?: AppleDistributionCertificateFragment[];
private _distributionCertificate?: AppleDistributionCertificateFragment;
constructor(private app: AppLookupParams) {}
public get distributionCertificate(): AppleDistributionCertificateQueryResult {
public get distributionCertificate(): AppleDistributionCertificateFragment {
assert(
this._distributionCertificate,
'distributionCertificate can be accessed only after calling .runAsync()'
@@ -58,7 +61,7 @@ export class SetupDistributionCertificate implements Action {
private async isCurrentCertificateValidAsync(
ctx: Context,
currentCertificate: AppleDistributionCertificateQueryResult | null
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<boolean> {
if (!currentCertificate) {
return false;
@@ -68,7 +71,7 @@ export class SetupDistributionCertificate implements Action {
);
const isValid = validCertSerialNumbers.includes(currentCertificate.serialNumber);
if (!isValid) {
log.warn('Current distribution certificate is no longer valid in Apple Dev Portal');
Log.warn('Current distribution certificate is no longer valid in Apple Dev Portal');
}
return isValid;
}
@@ -76,7 +79,7 @@ export class SetupDistributionCertificate implements Action {
private async createOrReuseDistCert(
manager: CredentialsManager,
ctx: Context
): Promise<AppleDistributionCertificateQueryResult> {
): Promise<AppleDistributionCertificateFragment> {
const validDistCerts = await this.getValidDistCertsAsync(ctx);
const autoselectedDistCert = validDistCerts[0];
@@ -87,7 +90,9 @@ export class SetupDistributionCertificate implements Action {
});
if (useAutoselected) {
log(`Using distribution certificate with serial number ${autoselectedDistCert.serialNumber}`);
Log.log(
`Using distribution certificate with serial number ${autoselectedDistCert.serialNumber}`
);
return autoselectedDistCert;
}
@@ -135,7 +140,7 @@ export class SetupDistributionCertificate implements Action {
private async getValidDistCertsAsync(
ctx: Context
): Promise<AppleDistributionCertificateQueryResult[]> {
): Promise<AppleDistributionCertificateFragment[]> {
if (this.validDistCerts) {
return this.validDistCerts;
}
@@ -146,7 +151,7 @@ export class SetupDistributionCertificate implements Action {
const validDistCertSerialNumberSet = new Set(validDistCertSerialNumbers);
const distCertsForAccount = await ctx.newIos.getDistributionCertificatesForAccountAsync(
this.app
this.app.account
);
const distCertsForAppleTeam = distCertsForAccount.filter(distCert => {
return (
@@ -0,0 +1,24 @@
import mockdate from 'mockdate';
import { AppleDistributionCertificateQuery } from '../../../api/graphql/queries/AppleDistributionCertificateQuery';
import { formatDistributionCertificate } from '../DistributionCertificateUtils';
jest.mock('../../../api/graphql/queries/AppleDistributionCertificateQuery');
jest.mock('chalk', () => {
return {
__esModule: true, // this property makes it work
default: { gray: jest.fn(log => log) },
};
});
mockdate.set(new Date('4/20/2021'));
describe('select credentials', () => {
it('select an AppleDistributionCertificate fragment', async () => {
const testDistCerts = (
await AppleDistributionCertificateQuery.getAllForAccount('quinAccount')
).sort((a, b) => (a.serialNumber > b.serialNumber ? 1 : -1));
const loggedSoFar = testDistCerts
.map(cert => formatDistributionCertificate(cert))
.reduce((acc, certLog) => acc + certLog);
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -0,0 +1,73 @@
import { confirmAsync } from '../../../../../prompts';
import { createCtxMock } from '../../../../__tests__/fixtures-context';
import {
testDistCertFragmentNoDependencies,
testDistCertFragmentOneDependency,
} from '../../../../__tests__/fixtures-ios';
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate';
jest.mock('../../../../../prompts');
(confirmAsync as jest.Mock).mockImplementation(() => true);
describe('RemoveDistributionCertificate', () => {
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const removeDistCertAction = new RemoveDistributionCertificate(
appLookupParams.account,
testDistCertFragmentNoDependencies
);
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect((ctx.newIos.deleteDistributionCertificateAsync as any).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect((ctx.appStore.revokeDistributionCertificateAsync as any).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate on Expo servers when there are no App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const removeDistCertAction = new RemoveDistributionCertificate(
appLookupParams.account,
testDistCertFragmentNoDependencies
);
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect((ctx.newIos.deleteDistributionCertificateAsync as any).mock.calls.length).toBe(1);
// not supported in non-interactive mode
expect((ctx.appStore.revokeDistributionCertificateAsync as any).mock.calls.length).toBe(0);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate and its provisioning profile on Expo and Apple servers when there are App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const removeDistCertAction = new RemoveDistributionCertificate(
appLookupParams.account,
testDistCertFragmentOneDependency
);
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect((ctx.newIos.deleteDistributionCertificateAsync as any).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect((ctx.appStore.revokeDistributionCertificateAsync as any).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be invoked on expo servers
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(1);
});
it('errors when the distribution certificate has App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const removeDistCertAction = new RemoveDistributionCertificate(
appLookupParams.account,
testDistCertFragmentOneDependency
);
// fail if users are trying to delete a dist cert with dependencies in non-interactive mode
await expect(removeDistCertAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -0,0 +1,22 @@
import { createCtxMock } from '../../../../__tests__/fixtures-context';
import { ManageIosBeta } from '../../../../manager/ManageIosBeta';
import { RemoveProvisioningProfiles } from '../RemoveProvisioningProfile';
describe('RemoveProvisioningProfile', () => {
it('Basic Case', async () => {
const ctx = createCtxMock();
const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx);
const testProvisioningProfile = {
id: 'test-id',
developerPortalIdentifier: 'test-developer-portal-identifier',
};
const removeProfilesAction = new RemoveProvisioningProfiles(
[appLookupParams],
[testProvisioningProfile]
);
await removeProfilesAction.runAsync(ctx);
// expect provisioning profile to be deleted from servers
expect((ctx.newIos.deleteProvisioningProfilesAsync as any).mock.calls.length).toBe(1);
});
});
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`select credentials select an AppleDistributionCertificate fragment 1`] = `
"Cert ID: 5URSS525PL, Serial number: 3F9CDD6CFC6E52C1, Team ID: 77KQ969CHE
Created: 6 months ago, Updated: 10 months ago,
Expires: Wed, 13 Oct 2021 18:28:44 GMT+0000Cert ID: JRH7292L8R, Serial number: 5544BE191B9F949E, Team ID: 77KQ969CHE
Created: 10 months ago, Updated: 10 months ago,
Expires: Tue, 22 Jun 2021 20:46:07 GMT+0000"
`;
@@ -3,6 +3,7 @@ import nullthrows from 'nullthrows';
import {
AppFragment,
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleTeamFragment,
CommonIosAppCredentialsFragment,
IosAppBuildCredentialsFragment,
@@ -28,10 +29,7 @@ import {
AppleDeviceFragmentWithAppleTeam,
AppleDeviceQuery,
} from './graphql/queries/AppleDeviceQuery';
import {
AppleDistributionCertificateQuery,
AppleDistributionCertificateQueryResult,
} from './graphql/queries/AppleDistributionCertificateQuery';
import { AppleDistributionCertificateQuery } from './graphql/queries/AppleDistributionCertificateQuery';
import {
AppleProvisioningProfileQuery,
AppleProvisioningProfileQueryResult,
@@ -242,11 +240,19 @@ export async function updateProvisioningProfileAsync(
);
}
export async function deleteProvisioningProfilesAsync(
appleProvisioningProfileIds: string[]
): Promise<void> {
return await AppleProvisioningProfileMutation.deleteAppleProvisioningProfilesAsync(
appleProvisioningProfileIds
);
}
export async function getDistributionCertificateForAppAsync(
appLookupParams: AppLookupParams,
appleTeam: AppleTeamFragment,
iosDistributionType: IosDistributionType
): Promise<AppleDistributionCertificateQueryResult | null> {
): Promise<AppleDistributionCertificateFragment | null> {
const projectFullName = formatProjectFullName(appLookupParams);
const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync(
appLookupParams,
@@ -258,9 +264,9 @@ export async function getDistributionCertificateForAppAsync(
});
}
export async function getDistributionCertificatesForAccountAsync({
account,
}: AppLookupParams): Promise<AppleDistributionCertificateQueryResult[]> {
export async function getDistributionCertificatesForAccountAsync(
account: Account
): Promise<AppleDistributionCertificateFragment[]> {
return await AppleDistributionCertificateQuery.getAllForAccount(account.name);
}
@@ -285,5 +291,13 @@ export async function createDistributionCertificateAsync(
);
}
export async function deleteDistributionCertificateAsync(
distributionCertificateId: string
): Promise<void> {
return await AppleDistributionCertificateMutation.deleteAppleDistributionCertificate(
distributionCertificateId
);
}
const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
`@${account.name}/${projectName}`;
@@ -7,6 +7,7 @@ import {
AppleDistributionCertificateFragment,
AppleTeamFragment,
CreateAppleDistributionCertificateMutation,
DeleteAppleDistributionCertificateMutation,
} from '../../../../../graphql/generated';
import { AppleDistributionCertificateFragmentNode } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
@@ -64,6 +65,28 @@ const AppleDistributionCertificateMutation = {
);
return data.appleDistributionCertificate.createAppleDistributionCertificate;
},
async deleteAppleDistributionCertificate(appleDistributionCertificateId: string): Promise<void> {
await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteAppleDistributionCertificateMutation>(
gql`
mutation DeleteAppleDistributionCertificateMutation(
$appleDistributionCertificateId: ID!
) {
appleDistributionCertificate {
deleteAppleDistributionCertificate(id: $appleDistributionCertificateId) {
id
}
}
}
`,
{
appleDistributionCertificateId,
}
)
.toPromise()
);
},
};
export { AppleDistributionCertificateMutation };
@@ -102,6 +102,26 @@ const AppleProvisioningProfileMutation = {
);
return data.appleProvisioningProfile.updateAppleProvisioningProfile;
},
async deleteAppleProvisioningProfilesAsync(appleProvisioningProfileIds: string[]): Promise<void> {
await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateAppleProvisioningProfileMutation>(
gql`
mutation DeleteAppleProvisioningProfilesMutation($appleProvisioningProfileIds: [ID!]!) {
appleProvisioningProfile {
deleteAppleProvisioningProfiles(ids: $appleProvisioningProfileIds) {
id
}
}
}
`,
{
appleProvisioningProfileIds,
}
)
.toPromise()
);
},
};
export { AppleProvisioningProfileMutation };
@@ -7,15 +7,11 @@ import {
AppleDistributionCertificateByAccountQuery,
AppleDistributionCertificateByAppQuery,
AppleDistributionCertificateFragment,
AppleTeamFragment,
IosDistributionType,
} from '../../../../../graphql/generated';
import { AppleDistributionCertificateFragmentNode } from '../../../../../graphql/types/credentials/AppleDistributionCertificate';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
export type AppleDistributionCertificateQueryResult = AppleDistributionCertificateFragment & {
appleTeam?: AppleTeamFragment | null;
};
const AppleDistributionCertificateQuery = {
async getForAppAsync(
projectFullName: string,
@@ -23,7 +19,7 @@ const AppleDistributionCertificateQuery = {
appleAppIdentifierId,
iosDistributionType,
}: { appleAppIdentifierId: string; iosDistributionType: IosDistributionType }
): Promise<AppleDistributionCertificateQueryResult | null> {
): Promise<AppleDistributionCertificateFragment | null> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDistributionCertificateByAppQuery>(
@@ -75,7 +71,7 @@ const AppleDistributionCertificateQuery = {
?.distributionCertificate ?? null
);
},
async getAllForAccount(accountName: string): Promise<AppleDistributionCertificateQueryResult[]> {
async getAllForAccount(accountName: string): Promise<AppleDistributionCertificateFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppleDistributionCertificateByAccountQuery>(
@@ -87,16 +83,11 @@ const AppleDistributionCertificateQuery = {
appleDistributionCertificates {
id
...AppleDistributionCertificateFragment
appleTeam {
id
...AppleTeamFragment
}
}
}
}
}
${print(AppleDistributionCertificateFragmentNode)}
${print(AppleTeamFragmentNode)}
`,
{
accountName,
@@ -0,0 +1,44 @@
const AppleDistributionCertificateQuery = {
getAllForAccount: jest.fn().mockImplementation(() => {
return [
{
id: '6a7422cc-392b-451d-984c-b0b4b027680a',
certificateP12: 'test-cert-p12-1',
certificatePassword: 'test-cert-password-1',
serialNumber: '5544BE191B9F949E',
developerPortalIdentifier: 'JRH7292L8R',
validityNotBefore: '2020-06-22T20:46:07.000Z',
validityNotAfter: '2021-06-22T20:46:07.000Z',
appleTeam: {
id: '6d39dbfe-a30a-44a4-bc36-d562e5963fa3',
appleTeamIdentifier: '77KQ969CHE',
appleTeamName: null,
__typename: 'AppleTeam',
},
iosAppBuildCredentialsList: [],
updatedAt: '2020-06-22T20:46:07.000Z',
__typename: 'AppleDistributionCertificate',
},
{
id: '8b3baa53-b149-4a1b-842b-75ff6e0bcb1e',
certificateP12: 'test-cert-p12-2',
certificatePassword: 'test-cert-password-2',
serialNumber: '3F9CDD6CFC6E52C1',
developerPortalIdentifier: '5URSS525PL',
validityNotBefore: '2020-10-13T18:28:44.000Z',
validityNotAfter: '2021-10-13T18:28:44.000Z',
appleTeam: {
id: '6d39dbfe-a30a-44a4-bc36-d562e5963fa3',
appleTeamIdentifier: '77KQ969CHE',
appleTeamName: null,
__typename: 'AppleTeam',
},
iosAppBuildCredentialsList: [],
updatedAt: '2020-06-22T20:46:07.000Z',
__typename: 'AppleDistributionCertificate',
},
];
}),
};
export { AppleDistributionCertificateQuery };
@@ -0,0 +1,22 @@
import { AppleDistributionCertificateFragment } from '../../../graphql/generated';
import { DistributionCertificateStoreInfo } from './Credentials.types';
export function filterRevokedDistributionCerts(
distributionCerts: AppleDistributionCertificateFragment[],
certInfoFromApple: DistributionCertificateStoreInfo[]
): AppleDistributionCertificateFragment[] {
if (distributionCerts.length === 0) {
return [];
}
// if the cert is valid, check it against apple to make sure it hasnt been revoked
const validCertSerialsOnAppleServer = certInfoFromApple
.filter(
// remove expired certs
cert => cert.expires > Math.floor(Date.now() / 1000)
)
.map(cert => cert.serialNumber);
return distributionCerts.filter(cert =>
validCertSerialsOnAppleServer.includes(cert.serialNumber)
);
}
@@ -8,7 +8,7 @@ import {
import assert from 'assert';
import chalk from 'chalk';
import log from '../../../log';
import Log from '../../../log';
import { toggleConfirmAsync } from '../../../prompts';
import {
deletePasswordAsync,
@@ -96,7 +96,7 @@ async function loginAsync(
});
} catch (error) {
if (error instanceof InvalidUserCredentialsError) {
log.error(error.message);
Log.error(error.message);
// Remove the invalid password so it isn't automatically used...
await deletePasswordAsync({ username });
@@ -145,7 +145,7 @@ async function loginWithUserCredentialsAsync({
export async function authenticateAsync(options: Options = {}): Promise<AuthCtx> {
// help keep apple login visually apart from the other operations.
log.addNewLineIfNone();
Log.addNewLineIfNone();
try {
const authState = await loginAsync(
@@ -182,7 +182,7 @@ export async function authenticateAsync(options: Options = {}): Promise<AuthCtx>
if (error.message === 'ABORTED') {
process.exit(1);
}
log(chalk.red('Authentication with Apple Developer Portal failed!'));
Log.log(chalk.red('Authentication with Apple Developer Portal failed!'));
throw error;
}
}
@@ -1,6 +1,6 @@
import { BundleId, Profile, RequestContext } from '@expo/apple-utils';
export async function getProfilesForBundleIdAsync(
async function getProfilesForBundleIdDangerousAsync(
context: RequestContext,
bundleIdentifier: string
): Promise<Profile[]> {
@@ -11,6 +11,35 @@ export async function getProfilesForBundleIdAsync(
return [];
}
export async function getProfilesForBundleIdAsync(
context: RequestContext,
bundleIdentifier: string
): Promise<Profile[]> {
const profiles = await getProfilesForBundleIdDangerousAsync(context, bundleIdentifier);
// users sometimes have a poisoned Apple cache and receive stale data from the API
// we call an arbitrary method, `getBundleIdAsync` on each profile
// if it errors, the profile was stale, so we remove it
const validProfileIds = new Set();
await Promise.all(
profiles.map(async profile => {
try {
await profile.getBundleIdAsync();
validProfileIds.add(profile.id);
} catch (e) {
if (
e.name === 'UnexpectedAppleResponse' &&
e.message.includes('The specified resource does not exist - There is no resource of type')
) {
// TODO: add tracking analytics here
return;
}
throw e;
}
})
);
return profiles.filter(profile => validProfileIds.has(profile.id));
}
export async function getBundleIdForIdentifierAsync(
context: RequestContext,
bundleIdentifier: string
@@ -2,7 +2,7 @@ import { ITCAgreements, RequestContext } from '@expo/apple-utils';
import chalk from 'chalk';
import { Ora } from 'ora';
import log from '../../../log';
import Log from '../../../log';
import { convertHTMLToASCII } from '../utils/convertHTMLToASCII';
async function getContractStatusAsync(
@@ -12,7 +12,7 @@ async function getContractStatusAsync(
const capabilities = await ITCAgreements.getCapabilitiesAsync(context);
return capabilities?.contractStatus ?? null;
} catch (error) {
log.warn(`Failed to get iTunes contract status: ${error.message}`);
Log.warn(`Failed to get iTunes contract status: ${error.message}`);
return null;
}
}
@@ -21,7 +21,7 @@ async function getContractMessagesAsync(context: RequestContext) {
try {
return await ITCAgreements.getContractMessagesAsync(context);
} catch (error) {
log.warn(`Failed to get iTunes contract messages: ${error.message}`);
Log.warn(`Failed to get iTunes contract messages: ${error.message}`);
return null;
}
}
@@ -55,10 +55,10 @@ async function getRequiredContractMessagesAsync(
// There is a small chance that this could result in a false positive if the messages are extraneous, so we'll also
// prompt the user to open an issue so we can address the new contract state if it ever appears.
// TODO: Maybe a silent analytic would be better
log.error(
Log.error(
`\nUnexpected Apple developer contract status "${status}". Please open an issue on https://github.com/expo/eas-cli`
);
log.newLine();
Log.newLine();
return { messages: (await getContractMessagesAsync(context)) ?? [], isFatal: false };
}
@@ -82,18 +82,18 @@ export async function assertContractMessagesAsync(context: RequestContext, spinn
if (spinner) {
spinner.stop();
}
log.newLine();
log(chalk.yellow.bold('Messages from App Store Connect:'));
log.newLine();
Log.newLine();
Log.log(chalk.yellow.bold('Messages from App Store Connect:'));
Log.newLine();
for (const message of messages) {
if (log.isDebug) {
log(JSON.stringify(message, null, 2));
log.newLine();
if (Log.isDebug) {
Log.log(JSON.stringify(message, null, 2));
Log.newLine();
}
log.addNewLineIfNone();
log(formatContractMessage(message));
Log.addNewLineIfNone();
Log.log(formatContractMessage(message));
}
log.addNewLineIfNone();
Log.addNewLineIfNone();
// Only throw an error if we know that the status is fatal, otherwise attempt to finish the process.
if (isFatal) {
throw new Error('App Store Connect has agreement updates that must be resolved');
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import dateformat from 'dateformat';
import ora from 'ora';
import log from '../../../log';
import Log from '../../../log';
import { PushKey, PushKeyStoreInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
@@ -67,7 +67,7 @@ export async function revokePushKeyAsync(authCtx: AuthCtx, ids: string[]): Promi
spinner.succeed(`Revoked ${name}`);
} catch (error) {
log.error(error);
Log.error(error);
spinner.fail(`Failed to revoke ${name}`);
throw error;
}
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import * as fs from 'fs-extra';
import wrapAnsi from 'wrap-ansi';
import log, { learnMore } from '../../../log';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import * as Keychain from './keychain';
@@ -44,7 +44,7 @@ function getAppleIdFromEnvironmentOrOptions({
}
async function promptUsernameAsync(): Promise<string> {
log('\u203A Log in to your Apple Developer account to continue');
Log.log('\u203A Log in to your Apple Developer account to continue');
// Get the email address that was last used and set it as
// the default value for quicker authentication.
@@ -80,13 +80,13 @@ export async function promptPasswordAsync({
const cachedPassword = await getCachedPasswordAsync({ username });
if (cachedPassword) {
log(`\u203A Using password for ${username} from your local Keychain`);
log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
Log.log(`\u203A Using password for ${username} from your local Keychain`);
Log.log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
return cachedPassword;
}
// https://docs.expo.io/distribution/security/#apple-developer-account-credentials
log(
Log.log(
wrapAnsi(
chalk.bold(
`\u203A The password is only used to authenticate with Apple and never stored on EAS servers`
@@ -94,7 +94,7 @@ export async function promptPasswordAsync({
process.stdout.columns || 80
)
);
log(` ${learnMore('https://bit.ly/2VtGWhU')}`);
Log.log(` ${learnMore('https://bit.ly/2VtGWhU')}`);
const { password } = await promptAsync({
type: 'password',
@@ -135,7 +135,7 @@ export async function deletePasswordAsync({
const serviceName = getKeychainServiceName(username);
const success = await Keychain.deletePasswordAsync({ username, serviceName });
if (success) {
log('\u203A Removed Apple ID password from the native Keychain');
Log.log('\u203A Removed Apple ID password from the native Keychain');
}
return success;
}
@@ -155,12 +155,12 @@ async function getCachedPasswordAsync({
async function cachePasswordAsync({ username, password }: Auth.UserCredentials): Promise<boolean> {
if (Keychain.EXPO_NO_KEYCHAIN) {
log('\u203A Skip storing Apple ID password in the local Keychain.');
Log.log('\u203A Skip storing Apple ID password in the local Keychain.');
return false;
}
log(`\u203A Saving Apple ID password to the local Keychain`);
log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
Log.log(`\u203A Saving Apple ID password to the local Keychain`);
Log.log(` ${learnMore('https://docs.expo.io/distribution/security#keychain')}`);
const serviceName = getKeychainServiceName(username);
return Keychain.setPasswordAsync({ username, password, serviceName });
}
@@ -1,5 +1,5 @@
import { AppleDevice } from '../../graphql/generated';
import log from '../../log';
import Log from '../../log';
import { CredentialSchema } from '../utils/promptForCredentials';
import {
DistributionCertificate,
@@ -89,9 +89,9 @@ export const distributionCertificateSchema: CredentialSchema<DistributionCertifi
const distCertSerialNumber = findP12CertSerialNumber(answers.certP12!, answers.certPassword!);
return { ...answers, distCertSerialNumber } as DistributionCertificate;
} catch (error) {
log.warn('Unable to access certificate serial number.');
log.warn('Make sure that certificate and password are correct.');
log.warn(error);
Log.warn('Unable to access certificate serial number.');
Log.warn('Make sure that certificate and password are correct.');
Log.warn(error);
}
return answers as DistributionCertificate;
},
@@ -1,22 +1,15 @@
import mockdate from 'mockdate';
import Log from '../../../../log';
import { IosAppCredentialsQuery } from '../../api/graphql/queries/IosAppCredentialsQuery';
import { displayIosAppCredentials } from '../printCredentialsBeta';
const mockLog = {
__esModule: true, // this property makes it work
default: jest.fn(toLog => toLog),
};
(mockLog.default as any).newLine = jest.fn();
jest.mock('../../../../log', () => mockLog);
jest.mock('chalk', () => {
return {
__esModule: true, // this property makes it work
default: { bold: jest.fn(log => log) },
};
});
jest.mock('../../api/graphql/queries/IosAppCredentialsQuery');
jest.mock('../../../../log');
jest.mock('chalk', () => ({ bold: jest.fn(log => log) }));
jest.mock('../../api/graphql/queries/IosAppCredentialsQuery');
mockdate.set(new Date('4/20/2021'));
describe('print credentials', () => {
it('prints the IosAppCredentials fragment', async () => {
const testIosAppCredentialsData = await IosAppCredentialsQuery.withCommonFieldsByAppIdentifierIdAsync(
@@ -26,9 +19,9 @@ describe('print credentials', () => {
}
);
displayIosAppCredentials(testIosAppCredentialsData);
const loggedSoFar = mockLog.default.mock.results
.map(mockCall => mockCall.value)
.reduce((acc, mockValue) => acc + mockValue);
const loggedSoFar = (Log.log as jest.Mock).mock.calls.reduce(
(acc, mockValue) => acc + mockValue
);
expect(loggedSoFar).toMatchSnapshot();
});
});
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more