mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 13:08:11 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58919d37ab | ||
|
|
2ec4279ac7 | ||
|
|
3052276c3b | ||
|
|
65a9238d48 | ||
|
|
5a1bb94563 | ||
|
|
1094d3f9f7 | ||
|
|
d8452ce6f8 | ||
|
|
a5be039fd2 | ||
|
|
6178a54292 |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.0"
|
||||
"version": "0.1.1"
|
||||
}
|
||||
+13
-13
@@ -21,7 +21,7 @@ $ npm install -g eas-cli
|
||||
$ eas COMMAND
|
||||
running command...
|
||||
$ eas (-v|--version|version)
|
||||
eas-cli/0.1.0 darwin-x64 node-v14.15.0
|
||||
eas-cli/0.1.1 darwin-x64 node-v14.15.0
|
||||
$ eas --help [COMMAND]
|
||||
USAGE
|
||||
$ eas COMMAND
|
||||
@@ -58,7 +58,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/account/login.ts)_
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -72,7 +72,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/account/logout.ts)_
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -86,7 +86,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/account/view.ts)_
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -105,7 +105,7 @@ OPTIONS
|
||||
--wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/build/index.ts)_
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/index.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -120,7 +120,7 @@ OPTIONS
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/build/configure.ts)_
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -136,7 +136,7 @@ OPTIONS
|
||||
--status=(in-queue|in-progress|errored|finished)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/build/list.ts)_
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILDID]`
|
||||
|
||||
@@ -147,7 +147,7 @@ USAGE
|
||||
$ eas build:view [BUILDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/build/view.ts)_
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -158,7 +158,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/credentials.ts)_
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -169,7 +169,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/device/create.ts)_
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -183,7 +183,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/device/list.ts)_
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -194,7 +194,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/device/view.ts)_
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -291,5 +291,5 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.1.0/build/commands/submit.ts)_
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/submit.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -3,4 +3,5 @@ export default jest.fn().mockReturnValue({
|
||||
fail: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
}),
|
||||
succeed: jest.fn(),
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
log.withTick('Successfully configured the Android project.');
|
||||
log.withTick('Android project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync(
|
||||
|
||||
@@ -212,7 +212,7 @@ async function reviewAndCommitChangesAsync(
|
||||
);
|
||||
} else if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync(commitMessage);
|
||||
log.withTick('Successfully committed changes.');
|
||||
log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(commitMessage, { nonInteractive, askedFirstTime: false });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -7,8 +7,12 @@ import log from '../log';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { platformDisplayNames } from './constants';
|
||||
|
||||
const USING_CREDENTIALS_JSON_MSG = 'Using credentials from the local credentials.json file';
|
||||
const USING_REMOTE_CREDENTIALS_MSG = 'Using credentials stored on the Expo servers';
|
||||
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);
|
||||
}
|
||||
|
||||
async function ensureCredentialsAutoAsync(
|
||||
provider: CredentialsProvider,
|
||||
@@ -41,7 +45,7 @@ async function ensureCredentialsAutoAsync(
|
||||
const { select } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
message: 'Which credentials you want to use for this build?',
|
||||
message: 'Which credentials do you want to use for this build?',
|
||||
choices: [
|
||||
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
|
||||
{ title: 'Credentials stored on Expo servers.', value: CredentialsSource.REMOTE },
|
||||
@@ -52,10 +56,10 @@ async function ensureCredentialsAutoAsync(
|
||||
return CredentialsSource.LOCAL;
|
||||
}
|
||||
} else if (hasLocal) {
|
||||
log(chalk.bold(USING_CREDENTIALS_JSON_MSG));
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
} else if (hasRemote) {
|
||||
log(chalk.bold(USING_REMOTE_CREDENTIALS_MSG));
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
@@ -69,7 +73,7 @@ async function ensureCredentialsAutoAsync(
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: 'Do you want to generate new credentials?',
|
||||
message: `${platform} credentials not found, generate now?`,
|
||||
});
|
||||
if (confirm) {
|
||||
return CredentialsSource.REMOTE;
|
||||
@@ -89,10 +93,10 @@ export async function ensureCredentialsAsync(
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
switch (src) {
|
||||
case CredentialsSource.LOCAL:
|
||||
log(chalk.bold(USING_CREDENTIALS_JSON_MSG));
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
case CredentialsSource.REMOTE:
|
||||
log(chalk.bold(USING_REMOTE_CREDENTIALS_MSG));
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
case CredentialsSource.AUTO:
|
||||
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
log.withTick('Successfully configured the iOS project.');
|
||||
log.withTick('iOS project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync(
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
isGitInstalledAsync,
|
||||
} from '../../utils/git';
|
||||
import { getTmpDirectory } from '../../utils/paths';
|
||||
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
||||
|
||||
async function ensureGitRepoExistsAsync(): Promise<void> {
|
||||
if (!(await isGitInstalledAsync())) {
|
||||
@@ -95,17 +96,41 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
|
||||
}
|
||||
|
||||
async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
const spinner = ora('Preparing project to be uploaded').start();
|
||||
const spinner = ora('Compressing project files');
|
||||
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const tarPath = path.join(getTmpDirectory(), `${uuidv4()}.tar.gz`);
|
||||
|
||||
await spawnAsync(
|
||||
'git',
|
||||
['archive', '--format=tar.gz', '--prefix', 'project/', '-o', tarPath, 'HEAD'],
|
||||
{ cwd: await gitRootDirectoryAsync() }
|
||||
// If the compression takes longer then a second, show the spinner.
|
||||
// This can happen when the user has a lot of resources or doesn't ignore their CocoaPods.
|
||||
// A basic project on a Mac can compress in roughly ~40ms.
|
||||
// A fairly complex project without CocoaPods ignored can take up to 30s.
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
spinner.start();
|
||||
},
|
||||
log.isDebug ? 1 : 1000
|
||||
);
|
||||
spinner.succeed('Project ready to be uploaded');
|
||||
// TODO: Possibly warn after more time about unoptimized assets.
|
||||
const compressTimerLabel = 'makeProjectTarballAsync';
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
try {
|
||||
await spawnAsync(
|
||||
'git',
|
||||
['archive', '--format=tar.gz', '--prefix', 'project/', '-o', tarPath, 'HEAD'],
|
||||
{ cwd: await gitRootDirectoryAsync() }
|
||||
);
|
||||
} finally {
|
||||
// Stop the timer
|
||||
clearTimeout(timer);
|
||||
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(`Compressed project files ${chalk.dim(prettyTime)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { size } = await fs.stat(tarPath);
|
||||
return { size, path: tarPath };
|
||||
|
||||
@@ -92,7 +92,7 @@ export default class Build extends Command {
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Choose the platforms to build',
|
||||
message: 'Build for platforms',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ If you are not sure what is the UDID of the device you are looking for, run:
|
||||
const device = await AppleDeviceQuery.getByDeviceIdentifier(accountName, UDID);
|
||||
|
||||
if (device) {
|
||||
spinner.succeed('Successfully fetched device details');
|
||||
spinner.succeed('Fetched device details');
|
||||
log(`\n${formatDevice(device, device.appleTeam)}`);
|
||||
} else {
|
||||
spinner.fail(`Couldn't find a device with the UDID ${UDID}`);
|
||||
|
||||
@@ -214,7 +214,7 @@ export default class BuildSubmit extends Command {
|
||||
async function promptForPlatformAsync(): Promise<SubmissionPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Choose a platform to submit to',
|
||||
message: 'Submit to platform',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
@@ -19,6 +17,6 @@ export class UpdateFcmKey implements Action {
|
||||
]);
|
||||
|
||||
await ctx.android.updateFcmKeyAsync(this.experienceName, fcmApiKey);
|
||||
log(chalk.green('Updated successfully'));
|
||||
log.succeed('Updated successfully');
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export class RemoveKeystore implements Action {
|
||||
await manager.runActionAsync(new BackupKeystore(this.projectFullName));
|
||||
|
||||
await ctx.android.removeKeystoreAsync(this.projectFullName);
|
||||
log(chalk.green('Keystore removed successfully.'));
|
||||
log.succeed('Keystore removed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
@@ -42,6 +40,6 @@ export class SetupBuildCredentialsFromCredentialsJson implements Action {
|
||||
await validateKeystoreAsync(localCredentials.keystore);
|
||||
}
|
||||
await ctx.android.updateKeystoreAsync(this.projectFullName, localCredentials.keystore);
|
||||
log(chalk.green('Keystore updated successfully.'));
|
||||
log.succeed('Keystore updated');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
@@ -11,10 +9,8 @@ export class UpdateCredentialsJson implements Action {
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
log('Updating Android credentials in credentials.json');
|
||||
await updateAndroidCredentialsAsync(ctx);
|
||||
log(
|
||||
chalk.green(
|
||||
'Android part of your local credentials.json is synced with values stored on Expo servers.'
|
||||
)
|
||||
log.succeed(
|
||||
'Android part of your local credentials.json is synced with values stored on Expo servers.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export class UpdateKeystore implements Action {
|
||||
const keystore = await this.provideOrGenerateAsync();
|
||||
|
||||
await ctx.android.updateKeystoreAsync(this.projectFullName, keystore);
|
||||
log(chalk.green('Keystore updated successfully.'));
|
||||
log.succeed('Keystore updated');
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(): Promise<Keystore> {
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('run UpdateKeystore when keystore exist on www', () => {
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
(prompts as any).mockImplementationOnce(() => ({ answer: false })); // promptForCredentials: let expo handle
|
||||
(prompts as any).mockImplementationOnce(() => ({ value: true })); // promptForCredentials: let expo handle
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('run UpdateKeystore when keystore exist on www', () => {
|
||||
|
||||
await fs.writeFile('/keystore.jks', testKeystore.keystore, 'base64');
|
||||
(prompts as any)
|
||||
.mockImplementationOnce(() => ({ answer: true })) // promptForCredentials: user specified
|
||||
.mockImplementationOnce(() => ({ value: false })) // promptForCredentials: user specified
|
||||
.mockImplementationOnce(() => ({ input: '/keystore.jks' })) // keystore
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // keystore password
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // key alias
|
||||
@@ -82,7 +82,7 @@ describe('run UpdateKeystore when keystore does not exist on www', () => {
|
||||
});
|
||||
const manager = createManagerMock();
|
||||
|
||||
(prompts as any).mockImplementationOnce(() => ({ answer: false })); // Let expo handle
|
||||
(prompts as any).mockImplementationOnce(() => ({ value: true })); // Let expo handle
|
||||
|
||||
await new UpdateKeystore(testExperienceName).runAsync(manager, ctx);
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('run UpdateKeystore when keystore does not exist on www', () => {
|
||||
|
||||
await fs.writeFile('/keystore.jks', testKeystore.keystore, 'base64');
|
||||
(prompts as any)
|
||||
.mockImplementationOnce(() => ({ answer: true })) // promptForCredentials: user specified
|
||||
.mockImplementationOnce(() => ({ value: false })) // promptForCredentials: user specified
|
||||
.mockImplementationOnce(() => ({ input: '/keystore.jks' })) // keystore
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // keystore password
|
||||
.mockImplementationOnce(() => ({ input: 'test' })) // key alias
|
||||
|
||||
@@ -20,9 +20,7 @@ export type AndroidCredentials = {
|
||||
export const keystoreSchema: CredentialSchema<Keystore> = {
|
||||
name: 'Android Keystore',
|
||||
provideMethodQuestion: {
|
||||
question: `Would you like to upload a Keystore or have us generate one for you?\nIf you don't know what this means, let us generate it! :)`,
|
||||
expoGenerated: 'Generate new keystore',
|
||||
userProvided: 'I want to upload my own file',
|
||||
question: `Generate a new Android Keystore?`,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
|
||||
@@ -13,10 +13,7 @@ export async function keytoolCommandExistsAsync(): Promise<boolean> {
|
||||
try {
|
||||
await spawnAsync('keytool');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
@@ -41,6 +40,6 @@ export class CreateDistributionCertificate implements Action {
|
||||
this.accountName,
|
||||
distCert
|
||||
);
|
||||
log(chalk.green('Successfully created Distribution Certificate.'));
|
||||
log.succeed('Created distribution certificate');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
@@ -29,7 +28,7 @@ export class CreateProvisioningProfile implements Action {
|
||||
const provisioningProfile = await this.provideOrGenerateAsync(ctx);
|
||||
await ctx.ios.updateProvisioningProfileAsync(this.app, provisioningProfile);
|
||||
|
||||
log(chalk.green('Successfully created Provisioning Profile.'));
|
||||
log.succeed('Created provisioning profile');
|
||||
}
|
||||
|
||||
private async provideOrGenerateAsync(ctx: Context): Promise<ProvisioningProfile> {
|
||||
|
||||
@@ -21,7 +21,7 @@ export class RemoveDistributionCertificate implements Action {
|
||||
await manager.runActionAsync(
|
||||
new RemoveSpecificDistributionCertificate(selected?.id, this.accountName, this.options)
|
||||
);
|
||||
log(chalk.green('Successfully removed Distribution Certificate.'));
|
||||
log.succeed('Removed distribution certificate');
|
||||
log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
@@ -32,16 +30,14 @@ export class RemoveSpecificProvisioningProfile implements Action {
|
||||
|
||||
async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
await ctx.ios.deleteProvisioningProfileAsync(this.app);
|
||||
log(
|
||||
chalk.green(
|
||||
`Successfully removed Provisioning Profile for @${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`
|
||||
)
|
||||
log.succeed(
|
||||
`Removed provisioning profile for @${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`
|
||||
);
|
||||
|
||||
let { shouldRevoke } = this.options;
|
||||
if (!shouldRevoke && !ctx.nonInteractive) {
|
||||
shouldRevoke = await confirmAsync({
|
||||
message: 'Do you also want to revoke this Provisioning Profile on Apple Developer Portal?',
|
||||
message: 'Do you also want to revoke this provisioning profile on Apple Developer Portal?',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export class SetupBuildCredentials implements Action {
|
||||
await ctx.bestEffortAppStoreAuthenticateAsync();
|
||||
|
||||
if (ctx.appStore.authCtx) {
|
||||
await ctx.appStore.ensureAppExistsAsync(this.app, { enablePushNotifications: true });
|
||||
await ctx.appStore.ensureBundleIdExistsAsync(this.app, { enablePushNotifications: true });
|
||||
}
|
||||
|
||||
let iosAppBuildCredentials: IosAppBuildCredentials | null = null;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { ensureAppIdentifierIsDefinedAsync } from '../../../project/projectUtils';
|
||||
@@ -15,10 +14,8 @@ export class UpdateCredentialsJson implements Action {
|
||||
const bundleIdentifer = await ensureAppIdentifierIsDefinedAsync(ctx.projectDir, Platform.iOS);
|
||||
log('Updating iOS credentials in credentials.json');
|
||||
await updateIosCredentialsAsync(ctx, bundleIdentifer);
|
||||
log(
|
||||
chalk.green(
|
||||
'iOS part of your local credentials.json is synced with values stored on Expo servers.'
|
||||
)
|
||||
log.succeed(
|
||||
'iOS part of your local credentials.json is synced with values stored on Expo servers.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,12 +72,12 @@ export class UpdateSpecificDistributionCertificate implements Action {
|
||||
this.accountName,
|
||||
newDistCert
|
||||
);
|
||||
log(chalk.green('Successfully updated Distribution Certificate'));
|
||||
log.succeed('Updated distribution certificate');
|
||||
log.newLine();
|
||||
|
||||
for (const appCredentials of apps) {
|
||||
log(
|
||||
`Removing Provisioning Profile for ${appCredentials.experienceName} (${appCredentials.bundleIdentifier})`
|
||||
`Removing provisioning profile for ${appCredentials.experienceName} (${appCredentials.bundleIdentifier})`
|
||||
);
|
||||
const appLookupParams = getAppLookupParams(
|
||||
appCredentials.experienceName,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import log from '../../../log';
|
||||
import { Action, CredentialsManager } from '../../CredentialsManager';
|
||||
import { Context } from '../../context';
|
||||
@@ -29,10 +27,8 @@ export class UseSpecificDistributionCertificate implements Action {
|
||||
|
||||
public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
|
||||
await ctx.ios.useDistributionCertificateAsync(this.app, this.userCredentialsId);
|
||||
log(
|
||||
chalk.green(
|
||||
`Successfully assigned Distribution Certificate to @${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`
|
||||
)
|
||||
log.succeed(
|
||||
`Assigned distribution certificate to @${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AppleDistributionCertificate } from '../../../../graphql/generated';
|
||||
import log from '../../../../log';
|
||||
@@ -31,6 +30,6 @@ export class CreateDistributionCertificate implements Action {
|
||||
this.app,
|
||||
distCert
|
||||
);
|
||||
log(chalk.green('Successfully created Distribution Certificate.'));
|
||||
log.succeed('Created distribution certificate');
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
listDistributionCertificatesAsync,
|
||||
revokeDistributionCertificateAsync,
|
||||
} from './distributionCertificate';
|
||||
import { AppLookupParams, EnsureAppExistsOptions, ensureAppExistsAsync } from './ensureAppExists';
|
||||
import {
|
||||
AppLookupParams,
|
||||
EnsureAppExistsOptions,
|
||||
ensureBundleIdExistsAsync,
|
||||
} from './ensureAppExists';
|
||||
import {
|
||||
ProfileClass,
|
||||
createProvisioningProfileAsync,
|
||||
@@ -48,12 +52,12 @@ class AppStoreApi {
|
||||
return this._authCtx;
|
||||
}
|
||||
|
||||
public async ensureAppExistsAsync(
|
||||
public async ensureBundleIdExistsAsync(
|
||||
app: AppLookupParams,
|
||||
options?: EnsureAppExistsOptions
|
||||
): Promise<void> {
|
||||
const ctx = await this.ensureAuthenticatedAsync();
|
||||
return await ensureAppExistsAsync(ctx, app, options);
|
||||
return await ensureBundleIdExistsAsync(ctx, app, options);
|
||||
}
|
||||
|
||||
public async listDistributionCertificatesAsync(): Promise<DistributionCertificateStoreInfo[]> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BundleId, CapabilityType, CapabilityTypeOption } from '@expo/apple-utils';
|
||||
import { App, BundleId, CapabilityType, CapabilityTypeOption } from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { AuthCtx, getRequestContext } from './authenticate';
|
||||
@@ -13,13 +14,28 @@ export interface AppLookupParams {
|
||||
bundleIdentifier: string;
|
||||
}
|
||||
|
||||
export async function ensureAppExistsAsync(
|
||||
export async function ensureBundleIdExistsAsync(
|
||||
authCtx: AuthCtx,
|
||||
{ accountName, projectName, bundleIdentifier }: AppLookupParams,
|
||||
options: EnsureAppExistsOptions = {}
|
||||
options?: EnsureAppExistsOptions
|
||||
) {
|
||||
return ensureBundleIdExistsWithNameAsync(
|
||||
authCtx,
|
||||
{
|
||||
name: `@${accountName}/${projectName}`,
|
||||
bundleIdentifier,
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureBundleIdExistsWithNameAsync(
|
||||
authCtx: AuthCtx,
|
||||
{ name, bundleIdentifier }: { name: string; bundleIdentifier: string },
|
||||
options?: EnsureAppExistsOptions
|
||||
) {
|
||||
const context = getRequestContext(authCtx);
|
||||
let spinner = ora(`Checking bundle identifier "${bundleIdentifier}"`).start();
|
||||
let spinner = ora(`Linking bundle identifier ${chalk.dim(bundleIdentifier)}`).start();
|
||||
|
||||
let bundleId: BundleId | null;
|
||||
try {
|
||||
@@ -27,38 +43,86 @@ export async function ensureAppExistsAsync(
|
||||
bundleId = await BundleId.findAsync(context, { identifier: bundleIdentifier });
|
||||
|
||||
if (!bundleId) {
|
||||
spinner.text = `Registering bundle identifier "${bundleIdentifier}"`;
|
||||
spinner.text = `Registering bundle identifier ${chalk.dim(bundleIdentifier)}`;
|
||||
// If it doesn't exist, create it
|
||||
bundleId = await BundleId.createAsync(context, {
|
||||
name: `@${accountName}/${projectName}`,
|
||||
name,
|
||||
identifier: bundleIdentifier,
|
||||
});
|
||||
}
|
||||
spinner.succeed(`Bundle identifier "${bundleIdentifier}" registered`);
|
||||
spinner.succeed(`Bundle identifier registered ${chalk.dim(bundleIdentifier)}`);
|
||||
} catch (err) {
|
||||
if (err.message.match(/An App ID with Identifier '(.*)' is not available/)) {
|
||||
spinner.fail(
|
||||
`The bundle identifier "${bundleIdentifier}" is not available to team "${authCtx.team.name}" (${authCtx.team.id}), please change it in your app config and try again.`
|
||||
`The bundle identifier ${chalk.bold(bundleIdentifier)} is not available to team "${
|
||||
authCtx.team.name
|
||||
}" (${authCtx.team.id}), please change it in your app config and try again.`
|
||||
);
|
||||
} else {
|
||||
spinner.fail('Failed to register bundle identifier');
|
||||
spinner.fail(`Failed to register bundle identifier ${chalk.dim(bundleIdentifier)}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
spinner = ora(`Syncing capabilities`).start();
|
||||
if (options) {
|
||||
try {
|
||||
spinner = ora(`Syncing capabilities`).start();
|
||||
|
||||
// Update the capabilities
|
||||
await bundleId.updateBundleIdCapabilityAsync({
|
||||
capabilityType: CapabilityType.PUSH_NOTIFICATIONS,
|
||||
option: options.enablePushNotifications ? CapabilityTypeOption.ON : CapabilityTypeOption.OFF,
|
||||
// TODO: Add more capabilities
|
||||
});
|
||||
spinner.succeed(`Synced capabilities`);
|
||||
} catch (err) {
|
||||
spinner.fail(`Failed to sync capabilities for "${bundleIdentifier}"`);
|
||||
// Update the capabilities
|
||||
await bundleId.updateBundleIdCapabilityAsync({
|
||||
capabilityType: CapabilityType.PUSH_NOTIFICATIONS,
|
||||
option: options.enablePushNotifications
|
||||
? CapabilityTypeOption.ON
|
||||
: CapabilityTypeOption.OFF,
|
||||
// TODO: Add more capabilities
|
||||
});
|
||||
spinner.succeed(`Synced capabilities`);
|
||||
} catch (err) {
|
||||
spinner.fail(`Failed to sync capabilities ${chalk.dim(bundleIdentifier)}`);
|
||||
|
||||
throw err;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureAppExistsAsync(
|
||||
authCtx: AuthCtx,
|
||||
{
|
||||
name,
|
||||
language,
|
||||
companyName,
|
||||
bundleIdentifier,
|
||||
}: { name: string; language?: string; companyName?: string; bundleIdentifier: string }
|
||||
) {
|
||||
const context = getRequestContext(authCtx);
|
||||
const spinner = ora(`Linking to App Store ${chalk.dim(bundleIdentifier)}`).start();
|
||||
|
||||
let app = await App.findAsync(context, { bundleId: bundleIdentifier });
|
||||
|
||||
if (!app) {
|
||||
spinner.text = `Creating App Store app ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`;
|
||||
try {
|
||||
app = await App.createAsync(context, {
|
||||
bundleId: bundleIdentifier,
|
||||
name,
|
||||
primaryLocale: language,
|
||||
companyName,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.match(/An App ID with Identifier '(.*)' is not available/)) {
|
||||
const providerName = authCtx.authState?.session.provider.name;
|
||||
throw new Error(
|
||||
`\nThe bundle identifier "${bundleIdentifier}" is not available to provider "${providerName}". Please change it in your app config and try again.\n`
|
||||
);
|
||||
}
|
||||
|
||||
spinner.fail(`Failed to create App Store app ${chalk.dim(name)}`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// TODO: Update app name when API gives us that possibility
|
||||
}
|
||||
spinner.succeed(`Prepared App Store for ${chalk.bold(name)} ${chalk.dim(bundleIdentifier)}`);
|
||||
return app;
|
||||
}
|
||||
@@ -44,8 +44,7 @@ function getAppleIdFromEnvironmentOrOptions({
|
||||
}
|
||||
|
||||
async function promptUsernameAsync(): Promise<string> {
|
||||
const wrap = wordwrap(process.stdout.columns || 80);
|
||||
log(wrap('Log in to your Apple Developer account to continue'));
|
||||
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.
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'path';
|
||||
import untildify from 'untildify';
|
||||
|
||||
import log from '../../log';
|
||||
import { Question as PromptQuestion, promptAsync } from '../../prompts';
|
||||
import { Question as PromptQuestion, confirmAsync, promptAsync } from '../../prompts';
|
||||
|
||||
export type Question = {
|
||||
field: string;
|
||||
@@ -18,8 +18,6 @@ export type CredentialSchema<T> = {
|
||||
questions: Question[];
|
||||
provideMethodQuestion?: {
|
||||
question?: string;
|
||||
expoGenerated?: string;
|
||||
userProvided?: string;
|
||||
};
|
||||
transformResultAsync?: (answers: Partial<T>) => Promise<T>;
|
||||
};
|
||||
@@ -28,7 +26,6 @@ const EXPERT_PROMPT = once(() =>
|
||||
log.warn(`
|
||||
In this mode, we won't be able to make sure that your credentials are valid.
|
||||
Please double check that you're uploading valid files for your app otherwise you may encounter strange errors!
|
||||
|
||||
When building for IOS make sure you've created your App ID on the Apple Developer Portal, that your App ID
|
||||
is in app.json as \`bundleIdentifier\`, and that the provisioning profile you
|
||||
upload matches that Team ID and App ID.
|
||||
@@ -39,11 +36,11 @@ export async function askForUserProvidedAsync<T>(
|
||||
schema: CredentialSchema<T>,
|
||||
initialValues: Partial<T> = {}
|
||||
): Promise<T | null> {
|
||||
if (await willUserProvideCredentialsAsync<T>(schema)) {
|
||||
EXPERT_PROMPT();
|
||||
return await getCredentialsFromUserAsync<T>(schema, initialValues);
|
||||
if (await shouldAutoGenerateCredentialsAsync<T>(schema)) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
EXPERT_PROMPT();
|
||||
return await getCredentialsFromUserAsync<T>(schema, initialValues);
|
||||
}
|
||||
|
||||
export async function getCredentialsFromUserAsync<T>(
|
||||
@@ -62,21 +59,10 @@ export async function getCredentialsFromUserAsync<T>(
|
||||
: (results as T);
|
||||
}
|
||||
|
||||
async function willUserProvideCredentialsAsync<T>(schema: CredentialSchema<T>) {
|
||||
const { answer } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'answer',
|
||||
message: schema?.provideMethodQuestion?.question ?? `Will you provide your own ${schema.name}?`,
|
||||
choices: [
|
||||
{
|
||||
title: schema?.provideMethodQuestion?.expoGenerated ?? 'Let EAS handle the process',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
title: schema?.provideMethodQuestion?.userProvided ?? 'I want to upload my own file',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
async function shouldAutoGenerateCredentialsAsync<T>(schema: CredentialSchema<T>) {
|
||||
const answer = await confirmAsync({
|
||||
message: schema?.provideMethodQuestion?.question ?? `Generate a new ${schema.name}?`,
|
||||
initial: true,
|
||||
});
|
||||
return answer;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import { boolish } from 'getenv';
|
||||
import ora from 'ora';
|
||||
|
||||
type Color = (...text: string[]) => string;
|
||||
|
||||
@@ -68,6 +69,10 @@ log.gray = function (...args: any[]) {
|
||||
consoleLog(...withTextColor(args, chalk.gray));
|
||||
};
|
||||
|
||||
log.succeed = function (message: string) {
|
||||
ora().succeed(message);
|
||||
};
|
||||
|
||||
log.withTick = function (...args: any[]) {
|
||||
consoleLog(chalk.green(figures.tick), ...args);
|
||||
};
|
||||
|
||||
@@ -138,7 +138,7 @@ describe(getProjectAccountNameAsync, () => {
|
||||
}));
|
||||
asMock(getUserAsync).mockImplementation((): Actor | undefined => undefined);
|
||||
|
||||
expect(getProjectAccountNameAsync('/app')).rejects.toThrow(/logged in/);
|
||||
await expect(getProjectAccountNameAsync('/app')).rejects.toThrow(/Failed to access user data/);
|
||||
});
|
||||
|
||||
it(`throws when project owner is undefined for robot actors`, async () => {
|
||||
@@ -154,6 +154,8 @@ describe(getProjectAccountNameAsync, () => {
|
||||
{ id: 'account_id_2', name: 'dominik' },
|
||||
],
|
||||
}));
|
||||
expect(getProjectAccountNameAsync('/app')).rejects.toThrow('manifest property is required');
|
||||
await expect(getProjectAccountNameAsync('/app')).rejects.toThrow(
|
||||
'manifest property is required'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -19,11 +19,11 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
|
||||
const { accountName, projectName } = projectInfo;
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
|
||||
const spinner = ora(`Ensuring ${chalk.bold(projectFullName)} is created on Expo`).start();
|
||||
const spinner = ora(`Linking to project ${chalk.bold(projectFullName)}`).start();
|
||||
|
||||
try {
|
||||
const id = await findProjectIdByAccountNameAndSlugAsync(accountName, projectName);
|
||||
spinner.succeed();
|
||||
spinner.succeed(`Linked to project ${chalk.bold(projectFullName)}`);
|
||||
return id;
|
||||
} catch (err) {
|
||||
if (err.graphQLErrors?.some((it: any) => it.extensions?.errorCode !== 'EXPERIENCE_NOT_FOUND')) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ora from 'ora';
|
||||
|
||||
import log from '../log';
|
||||
import { sleep } from '../utils/promise';
|
||||
import SubmissionService, { DEFAULT_CHECK_INTERVAL_MS } from './SubmissionService';
|
||||
import { Submission, SubmissionConfig, SubmissionStatus } from './SubmissionService.types';
|
||||
@@ -21,6 +22,7 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
|
||||
submissionConfig: SubmissionConfig,
|
||||
verbose: boolean = false
|
||||
) {
|
||||
log.addNewLineIfNone();
|
||||
const scheduleSpinner = ora('Scheduling submission').start();
|
||||
let submissionId: string;
|
||||
try {
|
||||
@@ -71,7 +73,7 @@ abstract class BaseSubmitter<SubmissionContext, SubmissionOptions> {
|
||||
} else if (status === SubmissionStatus.IN_PROGRESS) {
|
||||
return `Submitting your app to ${this.appStoreName}: submission in progress`;
|
||||
} else if (status === SubmissionStatus.FINISHED) {
|
||||
return `Successfully submitted your app to ${this.appStoreName}!`;
|
||||
return `Submitted your app to ${this.appStoreName}!`;
|
||||
} else if (status === SubmissionStatus.ERRORED) {
|
||||
return `Something went wrong when submitting your app to ${this.appStoreName}.`;
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ArchiveType,
|
||||
SubmissionPlatform,
|
||||
} from '../types';
|
||||
import { breakWord, printSummary } from '../utils/summary';
|
||||
import { printSummary } from '../utils/summary';
|
||||
import { AndroidPackageSource, getAndroidPackageAsync } from './AndroidPackageSource';
|
||||
import { AndroidSubmissionConfig, ReleaseStatus, ReleaseTrack } from './AndroidSubmissionConfig';
|
||||
import { ServiceAccountSource, getServiceAccountAsync } from './ServiceAccountSource';
|
||||
@@ -68,19 +68,34 @@ class AndroidSubmitter extends BaseSubmitter<AndroidSubmissionContext, AndroidSu
|
||||
projectId,
|
||||
};
|
||||
|
||||
printSummary(
|
||||
{
|
||||
...submissionConfig,
|
||||
serviceAccountPath,
|
||||
},
|
||||
'Android Submission Summary',
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
printSummaryAndroid({
|
||||
...submissionConfig,
|
||||
serviceAccountPath,
|
||||
});
|
||||
return { ...submissionConfig, serviceAccount };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the summary as a table. Exported for testing locally.
|
||||
*
|
||||
* @example
|
||||
* printSummaryAndroid({
|
||||
* androidPackage: 'com.expo.demoapp',
|
||||
* archivePath: '/Users/example/Documents/863f9337-65d2-40c6-acb3-c1054c5c09f8.apk',
|
||||
* archiveUrl: 'https://turtle-v2-artifacts.s3.amazonaws.com/ios/6420592d-5b5d-439b-aed4-ccd278647138-ca4145d8468947df9ded737248a1a238.aab',
|
||||
* archiveType: 'apk' as any,
|
||||
* serviceAccountPath: '/Users/example/Documents/gsa.json',
|
||||
* track: ReleaseTrack.production,
|
||||
* releaseStatus: ReleaseStatus.completed,
|
||||
* projectId: '863f9337-65d2-40c6-acb3-c1054c5c09f8',
|
||||
* });
|
||||
* @param submissionConfig
|
||||
*/
|
||||
export function printSummaryAndroid(submissionConfig: Summary) {
|
||||
printSummary(submissionConfig, SummaryHumanReadableKeys, SummaryHumanReadableValues);
|
||||
}
|
||||
|
||||
interface Summary {
|
||||
androidPackage: string;
|
||||
archivePath?: string;
|
||||
@@ -95,7 +110,7 @@ interface Summary {
|
||||
const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
|
||||
androidPackage: 'Android package',
|
||||
archivePath: 'Archive path',
|
||||
archiveUrl: 'Archive URL',
|
||||
archiveUrl: 'Download URL',
|
||||
archiveType: 'Archive type',
|
||||
serviceAccountPath: 'Google Service Account',
|
||||
track: 'Release track',
|
||||
@@ -103,9 +118,6 @@ const SummaryHumanReadableKeys: Record<keyof Summary, string> = {
|
||||
projectId: 'Project ID',
|
||||
};
|
||||
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {
|
||||
archivePath: (path: string) => breakWord(path, 50),
|
||||
archiveUrl: (url: string) => breakWord(url, 50),
|
||||
};
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof Summary, Function>> = {};
|
||||
|
||||
export default AndroidSubmitter;
|
||||
@@ -159,15 +159,15 @@ async function handlePromptSourceAsync(source: ArchiveFilePromptSource): Promise
|
||||
type: 'select',
|
||||
message: 'What would you like to submit?',
|
||||
choices: [
|
||||
{
|
||||
title: 'Latest build from EAS',
|
||||
value: ArchiveFileSourceType.latest,
|
||||
},
|
||||
{ title: 'I have a url to the app archive', value: ArchiveFileSourceType.url },
|
||||
{
|
||||
title: "I'd like to upload the app archive from my computer",
|
||||
title: 'Local app binary file',
|
||||
value: ArchiveFileSourceType.path,
|
||||
},
|
||||
{
|
||||
title: 'The latest build from Expo servers',
|
||||
value: ArchiveFileSourceType.latest,
|
||||
},
|
||||
{
|
||||
title: 'A build identified by a build id',
|
||||
value: ArchiveFileSourceType.buildId,
|
||||
|
||||
@@ -4,6 +4,10 @@ import { Platform } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
||||
import {
|
||||
ensureAppExistsAsync,
|
||||
ensureBundleIdExistsWithNameAsync,
|
||||
} from '../../credentials/ios/appstore/ensureAppExists';
|
||||
import log from '../../log';
|
||||
import { getAppIdentifierAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -80,10 +84,9 @@ async function createAppStoreConnectAppAsync(options: CreateAppOptions): Promise
|
||||
log.addNewLineIfNone();
|
||||
|
||||
if (await isProvisioningAvailableAsync(requestCtx)) {
|
||||
log(`Ensuring that bundle ID "${bundleId}" is registered on Apple Dev Center...`);
|
||||
await App.ensureBundleIdExistsAsync(requestCtx, {
|
||||
await ensureBundleIdExistsWithNameAsync(authCtx, {
|
||||
name: appName,
|
||||
bundleId,
|
||||
bundleIdentifier: bundleId,
|
||||
});
|
||||
} else {
|
||||
log.warn(
|
||||
@@ -91,56 +94,38 @@ async function createAppStoreConnectAppAsync(options: CreateAppOptions): Promise
|
||||
);
|
||||
}
|
||||
|
||||
log(`Checking App Store Connect for "${appName}" (${bundleId})...`);
|
||||
let app = await App.findAsync(requestCtx, { bundleId });
|
||||
let app: App | null = null;
|
||||
|
||||
if (!app) {
|
||||
log(`Creating app "${appName}" (${bundleId}) on App Store Connect...`);
|
||||
try {
|
||||
app = await App.createAsync(requestCtx, {
|
||||
bundleId,
|
||||
name: appName,
|
||||
primaryLocale: language,
|
||||
companyName,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.match(/An App ID with Identifier '(.*)' is not available/)) {
|
||||
const providerName = authCtx.authState?.session.provider.name;
|
||||
throw new Error(
|
||||
`\nThe bundle identifier "${bundleId}" is not available to provider "${providerName}". Please change it in your app config and try again.\n`
|
||||
);
|
||||
}
|
||||
|
||||
log.error('Failed to create the app in App Store Connect:');
|
||||
|
||||
if (
|
||||
// Name is invalid
|
||||
error.message.match(
|
||||
/App Name contains certain Unicode(.*)characters that are not permitted/
|
||||
) ||
|
||||
// UnexpectedAppleResponse: An attribute value has invalid characters. - App Name contains certain Unicode symbols, emoticons, diacritics, special characters, or private use characters that are not permitted.
|
||||
// Name is taken
|
||||
error.message.match(/The App Name you entered is already being used/)
|
||||
// UnexpectedAppleResponse: The provided entity includes an attribute with a value that has already been used on a different account. - The App Name you entered is already being used. If you have trademark rights to
|
||||
// this name and would like it released for your use, submit a claim.
|
||||
) {
|
||||
log.addNewLineIfNone();
|
||||
log.warn(
|
||||
`Change the name in your app config, or use a custom name with the ${chalk.bold(
|
||||
'--app-name'
|
||||
)} flag`
|
||||
);
|
||||
log.newLine();
|
||||
}
|
||||
throw error;
|
||||
try {
|
||||
app = await ensureAppExistsAsync(authCtx, {
|
||||
name: appName,
|
||||
language,
|
||||
companyName,
|
||||
bundleIdentifier: bundleId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
// Name is invalid
|
||||
error.message.match(
|
||||
/App Name contains certain Unicode(.*)characters that are not permitted/
|
||||
) ||
|
||||
// UnexpectedAppleResponse: An attribute value has invalid characters. - App Name contains certain Unicode symbols, emoticons, diacritics, special characters, or private use characters that are not permitted.
|
||||
// Name is taken
|
||||
error.message.match(/The App Name you entered is already being used/)
|
||||
// UnexpectedAppleResponse: The provided entity includes an attribute with a value that has already been used on a different account. - The App Name you entered is already being used. If you have trademark rights to
|
||||
// this name and would like it released for your use, submit a claim.
|
||||
) {
|
||||
log.addNewLineIfNone();
|
||||
log.warn(
|
||||
`Change the name in your app config, or use a custom name with the ${chalk.bold(
|
||||
'--app-name'
|
||||
)} flag`
|
||||
);
|
||||
log.newLine();
|
||||
}
|
||||
} else {
|
||||
// TODO: Update app name when API gives us that possibility
|
||||
throw error;
|
||||
}
|
||||
|
||||
// TODO: Maybe sync capabilities now as well
|
||||
log(`App "${app.attributes.name}" (${bundleId}) on App Store Connect is ready for your binary.`);
|
||||
|
||||
return {
|
||||
appleId: authCtx.appleId,
|
||||
ascAppId: app.id,
|
||||
@@ -151,7 +136,7 @@ async function promptForAppNameAsync(): Promise<string> {
|
||||
const { appName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'appName',
|
||||
message: 'How would you like to name your app?',
|
||||
message: 'What would you like to name your app?',
|
||||
validate: (val: string) => val !== '' || 'App name cannot be empty!',
|
||||
});
|
||||
return appName;
|
||||
|
||||
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
||||
import BaseSubmitter from '../BaseSubmitter';
|
||||
import { Archive, ArchiveSource, getArchiveAsync } from '../archiveSource';
|
||||
import { IosSubmissionContext, SubmissionPlatform } from '../types';
|
||||
import { breakWord, printSummary } from '../utils/summary';
|
||||
import { printSummary } from '../utils/summary';
|
||||
import {
|
||||
AppSpecificPasswordSource,
|
||||
getAppSpecificPasswordAsync,
|
||||
@@ -35,12 +35,8 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
resolvedSourceOptions
|
||||
);
|
||||
|
||||
printSummary(
|
||||
submissionConfig,
|
||||
'iOS Submission Summary',
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
printSummaryIOS(submissionConfig);
|
||||
|
||||
await this.startSubmissionAsync(submissionConfig, this.ctx.commandFlags.verbose);
|
||||
}
|
||||
|
||||
@@ -72,16 +68,37 @@ class IosSubmitter extends BaseSubmitter<IosSubmissionContext, IosSubmissionOpti
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the summary as a table. Exported for testing locally.
|
||||
* @example
|
||||
* printSummaryIOS({
|
||||
* appleId: '[email protected]',
|
||||
* appSpecificPassword: 'Apple Pie^',
|
||||
* appAppleId: '1234567890',
|
||||
* projectId: '863f9337-65d2-40c6-acb3-c1054c5c09f8',
|
||||
* archiveUrl: 'https://turtle-v2-artifacts.s3.amazonaws.com/ios/6420592d-5b5d-439b-aed4-ccd278647138-ca4145d8468947df9ded737248a1a238.ipa',
|
||||
* });
|
||||
* @param submissionConfig
|
||||
*/
|
||||
export function printSummaryIOS(submissionConfig: IosSubmissionConfig) {
|
||||
const { appSpecificPassword, projectId, archiveUrl, ...logConfig } = submissionConfig;
|
||||
|
||||
printSummary(
|
||||
{ ...logConfig, projectId, archiveUrl },
|
||||
SummaryHumanReadableKeys,
|
||||
SummaryHumanReadableValues
|
||||
);
|
||||
}
|
||||
|
||||
const SummaryHumanReadableKeys: Record<keyof IosSubmissionConfig, string> = {
|
||||
appleId: 'Apple ID',
|
||||
archiveUrl: 'Archive URL',
|
||||
archiveUrl: 'Download URL',
|
||||
appSpecificPassword: 'Apple app-specific password',
|
||||
appAppleId: 'App Store Connect App ID',
|
||||
appAppleId: 'ASC App ID',
|
||||
projectId: 'Project ID',
|
||||
};
|
||||
|
||||
const SummaryHumanReadableValues: Partial<Record<keyof IosSubmissionConfig, Function>> = {
|
||||
archiveUrl: (url: string) => breakWord(url, 50),
|
||||
appSpecificPassword: () => chalk.italic('[hidden]'),
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ enum SubmissionErrorCode {
|
||||
ANDROID_FIRST_UPLOAD_ERROR = 'SUBMISSION_SERVICE_ANDROID_FIRST_UPLOAD_ERROR',
|
||||
ANDROID_OLD_VERSION_CODE_ERROR = 'SUBMISSION_SERVICE_ANDROID_OLD_VERSION_CODE_ERROR',
|
||||
ANDROID_MISSING_PRIVACY_POLICY = 'SUBMISSION_SERVICE_ANDROID_MISSING_PRIVACY_POLICY',
|
||||
IOS_OLD_VERSION_CODE_ERROR = 'SUBMISSION_SERVICE_IOS_OLD_VERSION_CODE_ERROR',
|
||||
IOS_UNKNOWN_ERROR = 'SUBMISSION_SERVICE_IOS_UNKNOWN_ERROR',
|
||||
IOS_MISSING_APP_ICON = 'SUBMISSION_SERVICE_IOS_MISSING_APP_ICON',
|
||||
IOS_INVALID_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_SIGNATURE',
|
||||
}
|
||||
|
||||
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
@@ -28,6 +32,21 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
[SubmissionErrorCode.ANDROID_MISSING_PRIVACY_POLICY]:
|
||||
'The app has permissions that require a privacy policy set for the app.\n' +
|
||||
`${learnMore('https://expo.fyi/missing-privacy-policy')}.`,
|
||||
[SubmissionErrorCode.IOS_OLD_VERSION_CODE_ERROR]:
|
||||
"You've already submitted this version of the app.\n" +
|
||||
'Versions are identified by Build Numbers (expo.ios.buildNumber in app.json).\n' +
|
||||
"If you're submitting an Expo project built with EAS Build, increment the build number in app.json and build the project again." +
|
||||
`${learnMore('https://expo.fyi/bumping-ios-build-number')}.`,
|
||||
[SubmissionErrorCode.IOS_UNKNOWN_ERROR]:
|
||||
"We couldn't figure out what went wrong. Please see logs to learn more.",
|
||||
[SubmissionErrorCode.IOS_MISSING_APP_ICON]:
|
||||
'Your iOS app icon is missing or is an invalid format. The icon must be a 1024x1024 PNG image with no transparency.\n' +
|
||||
'Please check your icon image and icon configuration in app.json.\n' +
|
||||
`${learnMore('https://docs.expo.io/guides/app-icons/')}`,
|
||||
[SubmissionErrorCode.IOS_INVALID_SIGNATURE]:
|
||||
'Your app signature seems to be invalid.\n' +
|
||||
"Please check your iOS Distribution Certificate and your app's Provisioning Profile.\n" +
|
||||
`${learnMore('https://docs.expo.io/distribution/app-signing')}`,
|
||||
};
|
||||
|
||||
export function printSubmissionError(error: SubmissionError): boolean {
|
||||
@@ -35,7 +54,9 @@ export function printSubmissionError(error: SubmissionError): boolean {
|
||||
const errorCode = error.errorCode as SubmissionErrorCode;
|
||||
log.addNewLineIfNone();
|
||||
log.error(SubmissionErrorMessages[errorCode]);
|
||||
return errorCode === SubmissionErrorCode.ANDROID_UNKNOWN_ERROR;
|
||||
return [SubmissionErrorCode.ANDROID_UNKNOWN_ERROR, SubmissionErrorCode.IOS_UNKNOWN_ERROR].some(
|
||||
code => code === errorCode
|
||||
);
|
||||
} else {
|
||||
log(error.message);
|
||||
return true;
|
||||
|
||||
@@ -1,36 +1,31 @@
|
||||
import chalk from 'chalk';
|
||||
import Table from 'cli-table3';
|
||||
import chunk from 'lodash/chunk';
|
||||
|
||||
import log from '../../log';
|
||||
|
||||
export function breakWord(word: string, chars: number): string {
|
||||
return chunk(word, chars)
|
||||
.map((arr: string[]) => arr.join(''))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function printSummary<T>(
|
||||
summary: T,
|
||||
title: string,
|
||||
keyMap: Record<keyof T, string>,
|
||||
valueRemap: Partial<Record<keyof T, Function>>
|
||||
): void {
|
||||
const table = new Table({
|
||||
colWidths: [25, 55],
|
||||
wordWrap: true,
|
||||
});
|
||||
table.push([
|
||||
{
|
||||
colSpan: 2,
|
||||
content: chalk.bold(title),
|
||||
hAlign: 'center',
|
||||
},
|
||||
]);
|
||||
const padWidth = longestStringLength(Object.keys(summary).map(key => keyMap[key as keyof T]));
|
||||
|
||||
const tableFormat = (name: string, msg: string) =>
|
||||
`${chalk.bold.cyan(pad(`${name}:`, padWidth + 1))} ${msg}`;
|
||||
|
||||
log.newLine();
|
||||
for (const [key, value] of Object.entries(summary)) {
|
||||
const displayKey = keyMap[key as keyof T];
|
||||
const displayValue = valueRemap[key as keyof T]?.(value) ?? value;
|
||||
table.push([displayKey, displayValue]);
|
||||
log(tableFormat(displayKey, displayValue));
|
||||
}
|
||||
log(table.toString());
|
||||
log.addNewLineIfNone();
|
||||
}
|
||||
|
||||
function pad(str: string, width: number): string {
|
||||
const len = Math.max(0, width - str.length);
|
||||
return str + Array(len + 1).join(' ');
|
||||
}
|
||||
|
||||
function longestStringLength(values: string[]): number {
|
||||
return values.reduce((max, option) => Math.max(max, option.length), 0);
|
||||
}
|
||||
Reference in new issue
Block a user