Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

Commit

Permalink
[expo-cli] command for syncing credentials.json and credentials on www
Browse files Browse the repository at this point in the history
  • Loading branch information
wkozyra95 committed Aug 18, 2020
1 parent e7bf70a commit 51d7fdf
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 2 deletions.
73 changes: 73 additions & 0 deletions packages/expo-cli/src/commands/eas-build/credentialsSync/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import CommandError from '../../../CommandError';
import { Context } from '../../../credentials/context';
import { updateLocalCredentialsJsonAsync } from '../../../credentials/local';
import { runCredentialsManager } from '../../../credentials/route';
import { SetupAndroidBuildCredentialsFromLocal } from '../../../credentials/views/SetupAndroidKeystore';
import { SetupIosBuildCredentialsFromLocal } from '../../../credentials/views/SetupIosBuildCredentials';
import prompts from '../../../prompts';
import { BuildCommandPlatform } from '../types';

interface Options {
parent: {
nonInteractive?: boolean;
};
}

export default async function credentialsSyncAction(projectDir: string, options: Options) {
if (options.parent.nonInteractive) {
throw new CommandError('This command is not supported in --non-interactive mode');
}
const { update, platform } = await prompts([
{
type: 'select',
name: 'update',
message: 'What do you want to do?',
choices: [
{
title: 'Update credentials on Expo servers with local credentials.json content',
value: 'remote',
},
{ title: 'Update local credentials.json with values from Expo servers', value: 'local' },
],
},
{
type: 'select',
name: 'platform',
message: 'Do you want to update credentials for both platforms?',
choices: [
{ title: 'Android & iOS', value: BuildCommandPlatform.ALL },
{ title: 'only Android', value: BuildCommandPlatform.ANDROID },
{ title: 'only iOS', value: BuildCommandPlatform.IOS },
],
},
]);
if (update === 'local') {
await updateLocalCredentialsJsonAsync(projectDir, platform);
} else {
await updateRemoteCredentialsAsync(projectDir, platform);
}
}

async function updateRemoteCredentialsAsync(projectDir: string, platform: BuildCommandPlatform) {
const ctx = new Context();
await ctx.init(projectDir);
if (!ctx.hasProjectContext) {
throw new Error('project context is required'); // should bb checked earlier
}
if (['all', 'android'].includes(platform)) {
const experienceName = `@${ctx.manifest.owner || ctx.user.username}/${ctx.manifest.slug}`;
await runCredentialsManager(ctx, new SetupAndroidBuildCredentialsFromLocal(experienceName));
}
if (['all', 'ios'].includes(platform)) {
const bundleIdentifier = ctx.manifest.ios?.bundleIdentifier;
if (!bundleIdentifier) {
throw new Error('"expo.ios.bundleIdentifier" field is required in your app.json');
}
const appLookupParams = {
accountName: ctx.manifest.owner ?? ctx.user.username,
projectName: ctx.manifest.slug,
bundleIdentifier,
};
await runCredentialsManager(ctx, new SetupIosBuildCredentialsFromLocal(appLookupParams));
}
}
6 changes: 6 additions & 0 deletions packages/expo-cli/src/commands/eas-build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'fs-extra';
import path from 'path';

import buildAction from './build/action';
import credentialsSyncAction from './credentialsSync/action';
import statusAction from './status/action';

export default function (program: Command) {
Expand All @@ -12,6 +13,11 @@ export default function (program: Command) {
return;
}

program
.command('eas:credentials:sync [project-dir]')
.description('Update credentials.json with credentials stored on Expo servers')
.asyncActionProjectDir(credentialsSyncAction, { checkConfig: true });

program
.command('eas:build [project-dir]')
.description('Build an app binary for your project.')
Expand Down
1 change: 1 addition & 0 deletions packages/expo-cli/src/credentials/local/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as credentialsJson } from './credentialsJson';
export { updateLocalCredentialsJsonAsync } from './update';
157 changes: 157 additions & 0 deletions packages/expo-cli/src/credentials/local/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import fs from 'fs-extra';
import path from 'path';
import prompts from 'prompts';

import log from '../../log';
import { Context } from '../context';

type Platform = 'android' | 'ios' | 'all';

export async function updateLocalCredentialsJsonAsync(projectDir: string, platform: Platform) {
const ctx = new Context();
await ctx.init(projectDir);
if (!ctx.hasProjectContext) {
throw new Error('project context is required'); // should be checked earlier
}
if (['all', 'android'].includes(platform)) {
log('Updating Android credentials in credentials.json');
await updateAndroidAsync(ctx);
}
if (['all', 'ios'].includes(platform)) {
log('Updating iOS credentials in credentials.json');
await updateIosAsync(ctx);
}
}

async function updateAndroidAsync(ctx: Context) {
const credentialsJsonFilePath = path.join(ctx.projectDir, 'credentials.json');
let rawCredentialsJsonObject: any = {};
if (await fs.pathExists(credentialsJsonFilePath)) {
try {
const rawFile = await fs.readFile(credentialsJsonFilePath);
rawCredentialsJsonObject = JSON.parse(rawFile.toString());
} 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.');
throw error;
}
}
const experienceName = `@${ctx.manifest.owner || ctx.user.username}/${ctx.manifest.slug}`;
const keystore = await ctx.android.fetchKeystore(experienceName);
if (!keystore) {
throw new Error('There are no credentials configured for this project on Expo servers');
}

const isKeystoreComplete =
keystore.keystore && keystore.keystorePassword && keystore.keyPassword && keystore.keyAlias;

if (!isKeystoreComplete) {
const { confirm } = await prompts({
type: 'confirm',
name: 'confirm',
message:
'Credentials on Expo servers might be invalid or incomplete. Are you sure you want to continue?',
});
if (!confirm) {
log.warn('Aborting...');
return;
}
}

const keystorePath =
rawCredentialsJsonObject?.android?.keystorePath ?? './android/keystores/keystore.jks';
await _updateFileAsync(ctx.projectDir, keystorePath, keystore.keystore);

rawCredentialsJsonObject.android = {
keystore: {
keystorePath,
keystorePassword: keystore.keystorePassword,
keyAlias: keystore.keyAlias,
keyPassword: keystore.keyPassword,
},
};
await fs.writeJson(credentialsJsonFilePath, rawCredentialsJsonObject, {
spaces: 2,
});
}

async function updateIosAsync(ctx: Context) {
const credentialsJsonFilePath = path.join(ctx.projectDir, 'credentials.json');
let rawCredentialsJsonObject: any = {};
if (await fs.pathExists(credentialsJsonFilePath)) {
try {
const rawFile = await fs.readFile(credentialsJsonFilePath);
rawCredentialsJsonObject = JSON.parse(rawFile.toString());
} 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.');
throw error;
}
}

const bundleIdentifier = ctx.manifest.ios?.bundleIdentifier;
if (!bundleIdentifier) {
throw new Error('"expo.ios.bundleIdentifier" field is required in your app.json');
}
const appLookupParams = {
accountName: ctx.manifest.owner ?? ctx.user.username,
projectName: ctx.manifest.slug,
bundleIdentifier,
};
const pprofilePath =
rawCredentialsJsonObject?.ios?.provisioningProfilePath ?? './ios/certs/profile.mobileprovision';
const distCertPath =
rawCredentialsJsonObject?.ios?.distributionCertificate.path ?? './ios/certs/dist-cert.p12';
const appCredentials = await ctx.ios.getAppCredentials(appLookupParams);
const distCredentials = await ctx.ios.getDistCert(appLookupParams);
if (!appCredentials && !distCredentials) {
throw new Error('There are no credentials configured for this project on Expo servers');
}

const areCredentialsComplete =
appCredentials?.credentials?.provisioningProfile &&
distCredentials?.certP12 &&
distCredentials?.certPassword;

if (!areCredentialsComplete) {
const { confirm } = await prompts({
type: 'confirm',
name: 'confirm',
message:
'Credentials on Expo servers might be invalid or incomplete. Are you sure you want to continue?',
});
if (!confirm) {
log.warn('Aborting...');
return;
}
}

await _updateFileAsync(
ctx.projectDir,
pprofilePath,
appCredentials?.credentials?.provisioningProfile
);
await _updateFileAsync(ctx.projectDir, distCertPath, distCredentials?.certP12);

rawCredentialsJsonObject.ios = {
provisioningProfilePath: pprofilePath,
distributionCertificate: {
path: distCertPath,
password: distCredentials?.certPassword,
},
};
await fs.writeJson(credentialsJsonFilePath, rawCredentialsJsonObject, {
spaces: 2,
});
}

async function _updateFileAsync(projectDir: string, filePath: string, base64Data?: string) {
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
if (await fs.pathExists(absolutePath)) {
await fs.remove(absolutePath);
}
if (base64Data) {
await fs.mkdirp(path.dirname(filePath));
await fs.writeFile(filePath, Buffer.from(base64Data, 'base64'));
}
}
19 changes: 19 additions & 0 deletions packages/expo-cli/src/credentials/views/SetupAndroidKeystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import commandExists from 'command-exists';

import log from '../../log';
import { Context, IView } from '../context';
import { credentialsJson } from '../local';
import { UpdateKeystore } from './AndroidKeystore';

interface Options {
Expand Down Expand Up @@ -40,6 +41,24 @@ export class SetupAndroidKeystore implements IView {
}
}

export class SetupAndroidBuildCredentialsFromLocal implements IView {
constructor(private experienceName: string) {}

async open(ctx: Context): Promise<IView | null> {
let localCredentials;
try {
localCredentials = await credentialsJson.readAndroidAsync(ctx.projectDir);
} catch (error) {
log.error(
'Reading credentials from credentials.json failed. Make sure that file is correct and all credentials are present.'
);
throw error;
}
await ctx.android.updateKeystore(this.experienceName, localCredentials.keystore);
return null;
}
}

async function keytoolCommandExists(): Promise<boolean> {
try {
await commandExists('keytool');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import chalk from 'chalk';
import CommandError from '../../CommandError';
import * as appleApi from '../../appleApi';
import log from '../../log';
import prompt from '../../prompts';
import prompts from '../../prompts';
import { AppLookupParams } from '../api/IosApi';
import { Context, IView } from '../context';
import { credentialsJson } from '../local';
import { runCredentialsManager } from '../route';
import { readAppleTeam } from '../utils/provisioningProfile';
import { SetupIosDist } from './SetupIosDist';
import { SetupIosProvisioningProfile } from './SetupIosProvisioningProfile';

Expand Down Expand Up @@ -56,7 +58,7 @@ export class SetupIosBuildCredentials implements IView {
return;
}

const { confirm } = await prompt([
const { confirm } = await prompts([
{
type: 'confirm',
name: 'confirm',
Expand All @@ -74,3 +76,62 @@ export class SetupIosBuildCredentials implements IView {
}
}
}

export class SetupIosBuildCredentialsFromLocal implements IView {
constructor(private app: AppLookupParams) {}

async open(ctx: Context): Promise<IView | null> {
let localCredentials;
try {
localCredentials = await credentialsJson.readIosAsync(ctx.projectDir);
} catch (error) {
log.error(
'Reading credentials from credentials.json failed. Make sure that file is correct and all credentials are present.'
);
throw error;
}

const team = await readAppleTeam(localCredentials.provisioningProfile);
await ctx.ios.updateProvisioningProfile(this.app, {
...team,
provisioningProfile: localCredentials.provisioningProfile,
});
const credentials = await ctx.ios.getAllCredentials(this.app.accountName);
const distCert = await ctx.ios.getDistCert(this.app);
const appsUsingCert = distCert?.id
? (credentials.appCredentials || []).filter(cred => cred.distCredentialsId === distCert.id)
: [];

const appInfo = `@${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`;
const newDistCert = {
...team,
certP12: localCredentials.distributionCertificate.certP12,
certPassword: localCredentials.distributionCertificate.certPassword,
};

if (appsUsingCert.length > 1 && distCert?.id) {
const { update } = await prompts({
type: 'select',
name: 'update',
message:
'Current distribution certificate is used by multiple apps. Do you want to update all of them?',
choices: [
{ title: 'Update all apps', value: 'all' },
{ title: `Update only ${appInfo}`, value: 'app' },
],
});
if (update === 'all') {
await ctx.ios.updateDistCert(distCert.id, this.app.accountName, newDistCert);
} else {
const createdDistCert = await ctx.ios.createDistCert(this.app.accountName, newDistCert);
await ctx.ios.useDistCert(this.app, createdDistCert.id);
}
} else if (distCert?.id) {
await ctx.ios.updateDistCert(distCert.id, this.app.accountName, newDistCert);
} else {
const createdDistCert = await ctx.ios.createDistCert(this.app.accountName, newDistCert);
await ctx.ios.useDistCert(this.app, createdDistCert.id);
}
return null;
}
}

0 comments on commit 51d7fdf

Please sign in to comment.