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

[cli] fix publish:rollback #1707

Merged
merged 3 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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