@capawesome/cli
Version:
The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.
137 lines (136 loc) • 6.06 kB
JavaScript
import { DEFAULT_CONSOLE_BASE_URL } from '../../../config/consts.js';
import appChannelsService from '../../../services/app-channels.js';
import appDeploymentsService from '../../../services/app-deployments.js';
import { withAuth } from '../../../utils/auth.js';
import { isInteractive } from '../../../utils/environment.js';
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
import { formatTimeAgo } from '../../../utils/time-format.js';
import { defineCommand, defineOptions } from '@robingenz/zli';
import consola from 'consola';
import { z } from 'zod';
export default defineCommand({
description: 'Rollback the active build in a channel to a previous build.',
options: defineOptions(z.object({
appId: z
.uuid({
message: 'App ID must be a UUID.',
})
.optional()
.describe('App ID of the channel.'),
channel: z.string().optional().describe('Name of the channel to rollback.'),
steps: z.coerce
.number()
.int({
message: 'Steps must be an integer.',
})
.min(1, {
message: 'Steps must be at least 1.',
})
.max(5, {
message: 'Steps cannot be more than 5.',
})
.optional()
.describe('Number of deployments to go back (1-5).'),
})),
action: withAuth(async (options) => {
let { appId, channel, steps } = options;
// Prompt for app ID if not provided
if (!appId) {
if (!isInteractive()) {
consola.error('You must provide an app ID when running in non-interactive environment.');
process.exit(1);
}
const organizationId = await promptOrganizationSelection();
appId = await promptAppSelection(organizationId);
}
// Prompt for channel name if not provided
if (!channel) {
if (!isInteractive()) {
consola.error('You must provide a channel name when running in non-interactive environment.');
process.exit(1);
}
channel = await prompt('Enter the channel name to rollback:', {
type: 'text',
});
if (!channel) {
consola.error('You must enter a channel name to rollback.');
process.exit(1);
}
}
// Fetch channel by name
const appChannels = await appChannelsService.findAll({ appId, name: channel });
if (appChannels.length === 0) {
consola.error('Channel not found.');
process.exit(1);
}
const appChannelId = appChannels[0]?.id;
// Fetch deployments for the channel
const appDeployments = await appDeploymentsService.findAll({
appId,
appChannelId,
limit: 5,
relations: 'appBuild',
});
// Validate that we have at least 2 app deployments (current + previous)
if (appDeployments.length < 2) {
consola.error('Channel has no previous deployments to rollback to.');
process.exit(1);
}
// Select deployment to rollback to
let selectedIndex;
if (steps === undefined) {
// Interactive selection (exclude index 0 - current deployment)
if (!isInteractive()) {
consola.error('You must provide --steps when running in non-interactive environment.');
process.exit(1);
}
// Build options for select prompt (skip index 0)
const options = appDeployments.slice(1).map((deployment, index) => {
const appBuild = appDeployments[index + 1]?.appBuild;
if (!appBuild) {
consola.error('Deployment is missing associated build.');
process.exit(1);
}
const deployedTime = deployment.job?.createdAt ? formatTimeAgo(deployment.job.createdAt) : 'unknown';
return {
label: `Build #${appBuild.numberAsString} - Deployed ${deployedTime}`,
value: (index + 1).toString(),
};
});
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
const selectedValue = await prompt('Which deployment do you want to rollback to:', {
type: 'select',
options,
});
if (!selectedValue) {
consola.error('You must select a deployment to rollback to.');
process.exit(1);
}
selectedIndex = parseInt(selectedValue, 10);
}
else {
// Validate steps value
if (steps >= appDeployments.length) {
consola.error(`Cannot rollback ${steps} step${steps === 1 ? '' : 's'}, only ${appDeployments.length - 1} previous deployment${appDeployments.length - 1 === 1 ? '' : 's'} available.`);
process.exit(1);
}
selectedIndex = steps;
}
// Get the selected deployment and build
const selectedAppDeployment = appDeployments[selectedIndex];
if (!selectedAppDeployment) {
consola.error('Selected deployment not found.');
process.exit(1);
}
// Create new deployment with the selected build
consola.start('Creating rollback deployment...');
const response = await appDeploymentsService.create({
appId,
appBuildId: selectedAppDeployment.appBuildId,
appChannelName: channel,
});
consola.info(`Deployment ID: ${response.id}`);
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${response.id}`);
consola.success(`Rolled back to Build #${selectedAppDeployment.appBuild?.numberAsString} (${selectedIndex} step${selectedIndex === 1 ? '' : 's'} back).`);
}),
});