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

Commit

Permalink
[cli] fix publish:rollback (#1707)
Browse files Browse the repository at this point in the history
  • Loading branch information
quinlanj authored Apr 13, 2020
1 parent 814345d commit 133612c
Show file tree
Hide file tree
Showing 3 changed files with 442 additions and 147 deletions.
135 changes: 14 additions & 121 deletions packages/expo-cli/src/commands/publish-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,17 @@ import { Api, ApiV2, FormData, Project, UserManager } from '@expo/xdl';
import dateFormat from 'dateformat';

import * as table from './utils/cli-table';
import {
DetailOptions,
HistoryOptions,
Publication,
getPublicationDetailAsync,
getPublishHistoryAsync,
printPublicationDetailAsync,
} from './utils/PublishUtils';

const HORIZ_CELL_WIDTH_SMALL = 15;
const HORIZ_CELL_WIDTH_BIG = 40;
const VERSION = 2;

type HistoryOptions = {
releaseChannel?: string;
count?: number;
platform?: 'android' | 'ios';
raw?: boolean;
};

type DetailOptions = {
publishId?: string;
raw?: boolean;
};

type Publication = {
fullName: string;
channel: string;
channelId: string;
publicationId: string;
appVersion: string;
sdkVersion: string;
publishedTime: string;
platform: 'android' | 'ios';
};

export default (program: any) => {
program
Expand All @@ -45,53 +29,11 @@ export default (program: any) => {
'Number of logs to view, maximum 100, default 5.',
parseInt
)
.option('-p, --platform <ios|android>', 'Filter by platform, android or ios.')
.option('-p, --platform <ios|android>', 'Filter by platform, android or ios. Defaults to both platforms.')
.option('-s, --sdk-version <version>', 'Filter by SDK version e.g. 35.0.0')
.option('-r, --raw', 'Produce some raw output.')
.asyncActionProjectDir(async (projectDir: string, options: HistoryOptions) => {
if (options.count && (isNaN(options.count) || options.count < 1 || options.count > 100)) {
throw new Error('-n must be a number between 1 and 100 inclusive');
}

// TODO(ville): handle the API result for not authenticated user instead of checking upfront
const user = await UserManager.ensureLoggedInAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
});

let result: any;
if (process.env.EXPO_LEGACY_API === 'true') {
// TODO(ville): move request from multipart/form-data to JSON once supported by the endpoint.
let formData = new FormData();
formData.append('queryType', 'history');
if (exp.owner) {
formData.append('owner', exp.owner);
}
formData.append('slug', await Project.getSlugAsync(projectDir));
formData.append('version', VERSION);
if (options.releaseChannel) {
formData.append('releaseChannel', options.releaseChannel);
}
if (options.count) {
formData.append('count', options.count);
}
if (options.platform) {
formData.append('platform', options.platform);
}

result = await Api.callMethodAsync('publishInfo', [], 'post', null, {
formData,
});
} else {
const api = ApiV2.clientForUser(user);
result = await api.postAsync('publish/history', {
owner: exp.owner,
slug: await Project.getSlugAsync(projectDir),
version: VERSION,
releaseChannel: options.releaseChannel,
count: options.count,
platform: options.platform,
});
}
const result = await getPublishHistoryAsync(projectDir, options);

if (options.raw) {
console.log(JSON.stringify(result));
Expand All @@ -116,13 +58,12 @@ export default (program: any) => {
'sdkVersion',
'platform',
'channel',
'channelId',
'publicationId',
];

// colWidths contains the cell size of each header
let colWidths: number[] = [];
let bigCells = new Set(['publicationId', 'channelId', 'publishedTime']);
let bigCells = new Set(['publicationId', 'publishedTime']);
headers.forEach(header => {
if (bigCells.has(header)) {
colWidths.push(HORIZ_CELL_WIDTH_BIG);
Expand Down Expand Up @@ -151,55 +92,7 @@ export default (program: any) => {
throw new Error('--publish-id must be specified.');
}

// TODO(ville): handle the API result for not authenticated user instead of checking upfront
const user = await UserManager.ensureLoggedInAsync();
const { exp } = getConfig(projectDir, {
skipSDKVersionRequirement: true,
});
const slug = await Project.getSlugAsync(projectDir);

let result: any;
if (process.env.EXPO_LEGACY_API === 'true') {
let formData = new FormData();
formData.append('queryType', 'details');

if (exp.owner) {
formData.append('owner', exp.owner);
}
formData.append('publishId', options.publishId);
formData.append('slug', slug);

result = await Api.callMethodAsync('publishInfo', null, 'post', null, {
formData,
});
} else {
const api = ApiV2.clientForUser(user);
result = await api.postAsync('publish/details', {
owner: exp.owner,
publishId: options.publishId,
slug,
});
}

if (options.raw) {
console.log(JSON.stringify(result));
return;
}

if (result.queryResult) {
let queryResult = result.queryResult;
let manifest = queryResult.manifest;
delete queryResult.manifest;

// Print general release info
let generalTableString = table.printTableJson(queryResult, 'Release Description');
console.log(generalTableString);

// Print manifest info
let manifestTableString = table.printTableJson(manifest, 'Manifest Details');
console.log(manifestTableString);
} else {
throw new Error('No records found matching your query.');
}
const detail = await getPublicationDetailAsync(projectDir, options);
await printPublicationDetailAsync(detail, options);
});
};
149 changes: 123 additions & 26 deletions packages/expo-cli/src/commands/publish-modify.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { ApiV2, Project, UserManager } from '@expo/xdl';
import { Command } from 'commander';
import { uniqBy } from 'lodash';
import log from '../log';
import * as table from '../commands/utils/cli-table';
import {
Publication,
RollbackOptions,
getPublicationDetailAsync,
getPublishHistoryAsync,
rollbackPublicationFromChannelAsync,
setPublishToChannelAsync,
} from './utils/PublishUtils';

export default function(program: Command) {
program
Expand All @@ -27,14 +35,11 @@ export default function(program: Command) {
if (!options.publishId) {
throw new Error('You must specify a publish id. You can find ids using publish:history.');
}
const user = await UserManager.ensureLoggedInAsync();
const api = ApiV2.clientForUser(user);
try {
let result = await api.postAsync('publish/set', {
releaseChannel: options.releaseChannel,
publishId: options.publishId,
slug: await Project.getSlugAsync(projectDir),
});
const result = await setPublishToChannelAsync(
projectDir,
options as { releaseChannel: string; publishId: string }
);
let tableString = table.printTableJson(
result.queryResult,
'Channel Set Status ',
Expand All @@ -50,28 +55,120 @@ export default function(program: Command) {
.command('publish:rollback [project-dir]')
.alias('pr')
.description('Rollback an update to a channel.')
.option('--channel-id <channel-id>', 'The channel id to rollback in the channel. (Required)')
.option('--channel-id <channel-id>', 'This flag is deprecated.')
.option('-c, --release-channel <channel-name>', 'The channel to rollback from. (Required)')
.option('-s, --sdk-version <version>', 'The sdk version to rollback. (Required)')
.option('-p, --platform <ios|android>', 'The platform to rollback.')
.asyncActionProjectDir(
async (projectDir: string, options: { channelId?: string }): Promise<void> => {
if (!options.channelId) {
throw new Error('You must specify a channel id. You can find ids using publish:history.');
async (
projectDir: string,
options: {
releaseChannel?: string;
sdkVersion?: string;
platform?: string;
channelId?: string;
}
const user = await UserManager.getCurrentUserAsync();
const api = ApiV2.clientForUser(user);
try {
let result = await api.postAsync('publish/rollback', {
channelId: options.channelId,
slug: await Project.getSlugAsync(projectDir),
});
let tableString = table.printTableJson(
result.queryResult,
'Channel Rollback Status ',
'SUCCESS'
): Promise<void> => {
if (options.channelId) {
throw new Error(
'This flag is deprecated and does not do anything. Please use --release-channel and --sdk-version instead.'
);
console.log(tableString);
} catch (e) {
log.error(e);
}
if (!options.releaseChannel || !options.sdkVersion) {
const usage = await getUsageAsync(projectDir);
throw new Error(usage);
}
if (options.platform) {
if (options.platform !== 'android' && options.platform !== 'ios') {
throw new Error(
'Platform must be either android or ios. Leave out the platform flag to target both platforms.'
);
}
}
await rollbackPublicationFromChannelAsync(projectDir, options as RollbackOptions);
}
);
}
async function getUsageAsync(projectDir: string): Promise<string> {
try {
return await _getUsageAsync(projectDir);
} catch (e) {
log.warn(e);
// couldn't print out warning for some reason
return _getGenericUsage();
}
}

async function _getUsageAsync(projectDir: string): Promise<string> {
const allPlatforms = ['ios', 'android'];
const publishesResult = await getPublishHistoryAsync(projectDir, {
releaseChannel: 'default', // not specifying a channel will return most recent publishes but this is not neccesarily the most recent entry in a channel (user could have set an older publish to top of the channel)
count: allPlatforms.length,
});
const publishes = publishesResult.queryResult as Publication[];

// If the user published normally, there would be a publish for each platform with the same revisionId
const uniquePlatforms = uniqBy(publishes, publish => publish.platform);
if (uniquePlatforms.length !== allPlatforms.length) {
// User probably applied some custom `publish:set` or `publish:rollback` command
return _getGenericUsage();
}

const details = await Promise.all(
publishes.map(async publication => {
const detailOptions = {
publishId: publication.publicationId,
};
return await getPublicationDetailAsync(projectDir, detailOptions);
})
);

const uniqueRevisionIds = uniqBy(details, detail => detail.revisionId);
if (uniqueRevisionIds.length !== 1) {
// User probably applied some custom `publish:set` or `publish:rollback` command
return _getGenericUsage();
}

const { channel } = publishes[0];
const { revisionId, publishedTime, sdkVersion } = details[0];
const timeDifferenceString = _getTimeDifferenceString(new Date(), new Date(publishedTime));

return (
`--release-channel and --sdk-version arguments are required. \n` +
`For example, to roll back the revision [${revisionId}] on release channel [${channel}] (published ${timeDifferenceString}), \n` +
`run: expo publish:rollback --release-channel ${channel} --sdk-version ${sdkVersion}`
);
}

function _getTimeDifferenceString(t0: Date, t1: Date): string {
const minutesInMs = 60 * 1000;
const hourInMs = 60 * minutesInMs;
const dayInMs = 24 * hourInMs; // hours*minutes*seconds*milliseconds
const diffMs = Math.abs(t1.getTime() - t0.getTime());

const diffDays = Math.round(diffMs / dayInMs);
if (diffDays > 0) {
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
}

const diffHours = Math.round(diffMs / hourInMs);
if (diffHours > 0) {
return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
}

const diffMinutes = Math.round(diffMs / minutesInMs);
if (diffMinutes > 0) {
return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
}

return 'recently';
}

function _getGenericUsage(): string {
return (
`--release-channel and --sdk-version arguments are required. \n` +
`For example, to roll back the latest publishes on the default channel for sdk 37.0.0, \n` +
`run: expo publish:rollback --release-channel defaul --sdk-version 37.0.0 \n` +
`To rollback a specific platform, use the --platform flag.`
);
}
Loading

0 comments on commit 133612c

Please sign in to comment.