mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e5d3626f4 | ||
|
|
5808c5b412 | ||
|
|
cd64392d44 | ||
|
|
79f889e16c | ||
|
|
595bfecf1c | ||
|
|
ced2e192eb | ||
|
|
ee91612c07 | ||
|
|
4d815c1fd9 | ||
|
|
d2687a55b7 | ||
|
|
41142d33dc | ||
|
|
52ab6107c5 | ||
|
|
56ac348ed4 | ||
|
|
1ac0af53ab | ||
|
|
f9361b471c | ||
|
|
8f39e0dcde | ||
|
|
81de402d2d | ||
|
|
9d6e3f52d8 | ||
|
|
6a0a9bbaaa | ||
|
|
24bc29b3cd | ||
|
|
aac5ebff73 | ||
|
|
61cc5b0a4a | ||
|
|
1383783c33 | ||
|
|
971129be6b | ||
|
|
f4fb10483a | ||
|
|
256a0e1f22 | ||
|
|
b82f0b04fe | ||
|
|
9714420f90 | ||
|
|
0054f0bc26 | ||
|
|
38f565d849 | ||
|
|
1aff7ae2a3 | ||
|
|
31020047ca | ||
|
|
26778ec52b |
No files matched your search
@@ -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))
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.2.0"
|
||||
"version": "0.4.2"
|
||||
}
|
||||
+30
-36
@@ -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
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+31
-31
@@ -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}`
|
||||
)}.`
|
||||
);
|
||||
+22
-22
@@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
+21
-25
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-25
@@ -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}`)}.`
|
||||
);
|
||||
}
|
||||
+34
-34
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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!`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,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');
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
+24
@@ -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();
|
||||
});
|
||||
});
|
||||
+73
@@ -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();
|
||||
});
|
||||
});
|
||||
+22
@@ -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);
|
||||
});
|
||||
});
|
||||
+9
@@ -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}`;
|
||||
+23
@@ -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 };
|
||||
+20
@@ -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 };
|
||||
+2
-11
@@ -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,
|
||||
|
||||
+44
@@ -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
Reference in new issue
Block a user