@redocly/cli
Version:
[@Redocly](https://redocly.com) CLI is your all-in-one OpenAPI utility. It builds, manages, improves, and quality-checks your OpenAPI descriptions, all of which comes in handy for various phases of the API Lifecycle. Create your own rulesets to make API g
326 lines (293 loc) • 9.41 kB
text/typescript
import * as colors from 'colorette';
import { exitWithError, printExecutionTime } from '../../utils/miscellaneous';
import { Spinner } from '../../utils/spinner';
import { DeploymentError } from '../utils';
import { ReuniteApi, getApiKeys, getDomain } from '../api';
import { capitalize } from '../../utils/js-utils';
import { handleReuniteError, retryUntilConditionMet } from './utils';
import type { OutputFormat } from '@redocly/openapi-core';
import type { CommandArgs } from '../../wrapper';
import type {
DeploymentStatus,
DeploymentStatusResponse,
PushResponse,
ScorecardItem,
} from '../api/types';
const RETRY_INTERVAL_MS = 5000; // 5 sec
export type PushStatusOptions = {
organization: string;
project: string;
pushId: string;
domain?: string;
config?: string;
format?: Extract<OutputFormat, 'stylish'>;
wait?: boolean;
'max-execution-time'?: number; // in seconds
'retry-interval'?: number; // in seconds
'start-time'?: number; // in milliseconds
'continue-on-deploy-failures'?: boolean;
onRetry?: (lasSummary: PushStatusSummary) => void;
};
export interface PushStatusSummary {
preview: DeploymentStatusResponse;
production: DeploymentStatusResponse | null;
commit: PushResponse['commit'];
}
export async function handlePushStatus({
argv,
config,
version,
}: CommandArgs<PushStatusOptions>): Promise<PushStatusSummary | void> {
const startedAt = performance.now();
const spinner = new Spinner();
const { organization, project: projectId, pushId, wait } = argv;
const orgId = organization || config.organization;
if (!orgId) {
exitWithError(
`No organization provided, please use --organization option or specify the 'organization' field in the config file.`
);
return;
}
const domain = argv.domain || getDomain();
const maxExecutionTime = argv['max-execution-time'] || 1200; // 20 min
const retryIntervalMs = argv['retry-interval']
? argv['retry-interval'] * 1000
: RETRY_INTERVAL_MS;
const startTime = argv['start-time'] || Date.now();
const retryTimeoutMs = maxExecutionTime * 1000;
const continueOnDeployFailures = argv['continue-on-deploy-failures'] || false;
try {
const apiKey = getApiKeys(domain);
const client = new ReuniteApi({ domain, apiKey, version, command: 'push-status' });
let pushResponse: PushResponse;
pushResponse = await retryUntilConditionMet({
operation: () =>
client.remotes.getPush({
organizationId: orgId,
projectId,
pushId,
}),
condition: wait
? // Keep retrying if status is "pending" or "running" (returning false, so the operation will be retried)
(result) => !['pending', 'running'].includes(result.status['preview'].deploy.status)
: null,
onConditionNotMet: (lastResult) => {
displayDeploymentAndBuildStatus({
status: lastResult.status['preview'].deploy.status,
url: lastResult.status['preview'].deploy.url,
spinner,
buildType: 'preview',
continueOnDeployFailures,
wait,
});
},
onRetry: (lastResult) => {
if (argv.onRetry) {
argv.onRetry({
preview: lastResult.status.preview,
production: lastResult.isMainBranch ? lastResult.status.production : null,
commit: lastResult.commit,
});
}
},
startTime,
retryTimeoutMs,
retryIntervalMs,
});
printPushStatus({
buildType: 'preview',
spinner,
wait,
push: pushResponse,
continueOnDeployFailures,
});
printScorecard(pushResponse.status.preview.scorecard);
const shouldWaitForProdDeployment =
pushResponse.isMainBranch &&
(wait ? pushResponse.status.preview.deploy.status === 'success' : true);
if (shouldWaitForProdDeployment) {
pushResponse = await retryUntilConditionMet({
operation: () =>
client.remotes.getPush({
organizationId: orgId,
projectId,
pushId,
}),
condition: wait
? // Keep retrying if status is "pending" or "running" (returning false, so the operation will be retried)
(result) => !['pending', 'running'].includes(result.status['production'].deploy.status)
: null,
onConditionNotMet: (lastResult) => {
displayDeploymentAndBuildStatus({
status: lastResult.status['production'].deploy.status,
url: lastResult.status['production'].deploy.url,
spinner,
buildType: 'production',
continueOnDeployFailures,
wait,
});
},
onRetry: (lastResult) => {
if (argv.onRetry) {
argv.onRetry({
preview: lastResult.status.preview,
production: lastResult.isMainBranch ? lastResult.status.production : null,
commit: lastResult.commit,
});
}
},
startTime,
retryTimeoutMs,
retryIntervalMs,
});
}
if (pushResponse.isMainBranch) {
printPushStatus({
buildType: 'production',
spinner,
wait,
push: pushResponse,
continueOnDeployFailures,
});
printScorecard(pushResponse.status.production.scorecard);
}
printPushStatusInfo({ orgId, projectId, pushId, startedAt });
client.reportSunsetWarnings();
const summary: PushStatusSummary = {
preview: pushResponse.status.preview,
production: pushResponse.isMainBranch ? pushResponse.status.production : null,
commit: pushResponse.commit,
};
return summary;
} catch (err) {
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
handleReuniteError('✗ Failed to get push status.', err);
} finally {
spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
}
}
function printPushStatusInfo({
orgId,
projectId,
pushId,
startedAt,
}: {
orgId: string;
projectId: string;
pushId: string;
startedAt: number;
}) {
process.stderr.write(
`\nProcessed push-status for ${colors.yellow(orgId!)}, ${colors.yellow(
projectId
)} and pushID ${colors.yellow(pushId)}.\n`
);
printExecutionTime('push-status', startedAt, 'Finished');
}
function printPushStatus({
buildType,
spinner,
push,
continueOnDeployFailures,
}: {
buildType: 'preview' | 'production';
spinner: Spinner;
wait?: boolean;
push?: PushResponse | null;
continueOnDeployFailures: boolean;
}) {
if (!push) {
return;
}
if (push.isOutdated || !push.hasChanges) {
process.stderr.write(
colors.yellow(
`Files not added to your project. Reason: ${push.isOutdated ? 'outdated' : 'no changes'}.\n`
)
);
} else {
displayDeploymentAndBuildStatus({
status: push.status[buildType].deploy.status,
url: push.status[buildType].deploy.url,
buildType,
spinner,
continueOnDeployFailures,
});
}
}
function printScorecard(scorecard?: ScorecardItem[]) {
if (!scorecard || scorecard.length === 0) {
return;
}
process.stdout.write(`\n${colors.magenta('Scorecard')}:`);
for (const scorecardItem of scorecard) {
process.stdout.write(`
${colors.magenta('Name')}: ${scorecardItem.name}
${colors.magenta('Status')}: ${scorecardItem.status}
${colors.magenta('URL')}: ${colors.cyan(scorecardItem.url)}
${colors.magenta('Description')}: ${scorecardItem.description}\n`);
}
process.stdout.write(`\n`);
}
function displayDeploymentAndBuildStatus({
status,
url,
spinner,
buildType,
continueOnDeployFailures,
wait,
}: {
status: DeploymentStatus;
url: string | null;
spinner: Spinner;
buildType: 'preview' | 'production';
continueOnDeployFailures: boolean;
wait?: boolean;
}) {
const message = getMessage({ status, url, buildType, wait });
if (status === 'failed' && !continueOnDeployFailures) {
spinner.stop();
throw new DeploymentError(message);
}
if (wait && (status === 'pending' || status === 'running')) {
return spinner.start(message);
}
spinner.stop();
return process.stdout.write(message);
}
function getMessage({
status,
url,
buildType,
wait,
}: {
status: DeploymentStatus;
url: string | null;
buildType: 'preview' | 'production';
wait?: boolean;
}): string {
switch (status) {
case 'skipped':
return `${colors.yellow(`Skipped ${buildType}`)}\n`;
case 'pending': {
const message = `${colors.yellow(`Pending ${buildType}`)}`;
return wait ? message : `Status: ${message}\n`;
}
case 'running': {
const message = `${colors.yellow(`Running ${buildType}`)}`;
return wait ? message : `Status: ${message}\n`;
}
case 'success':
return `${colors.green(`🚀 ${capitalize(buildType)} deploy success.`)}\n${colors.magenta(
`${capitalize(buildType)} URL`
)}: ${colors.cyan(url || 'No URL yet.')}\n`;
case 'failed':
return `${colors.red(`❌ ${capitalize(buildType)} deploy fail.`)}\n${colors.magenta(
`${capitalize(buildType)} URL`
)}: ${colors.cyan(url || 'No URL yet.')}`;
default: {
const message = `${colors.yellow(`No status yet for ${buildType} deploy`)}`;
return wait ? message : `Status: ${message}\n`;
}
}
}