import { command, flag, help, namespace, param } from 'oo-cli'; import { handleInterrupt } from '../../lib/handleInterrupt'; import { die } from '../../lib/die'; import * as chalk from 'chalk'; import { Rivendell } from '../../lib/Rivendell'; import { shardsFromManifest } from '../../lib/Shards'; import { formatError } from '../../lib/formatError'; import { TerminalConfirm } from '../../lib/TerminalConfirm'; import {RivendellApi} from '../../lib/RivendellApi'; import AppVersionState = Rivendell.AppVersionState; import ApiError = RivendellApi.ApiError; import {constants as httpConstants} from 'http2'; const ABANDONABLE_STATES = [ AppVersionState.NEW, AppVersionState.BUILD_FAILED, AppVersionState.PUBLISHED ]; @namespace('app') export class AbandonCommand { @param @help('The App ID and version (e.g. my_app@1.0.0)') public appVersion !: string; @flag('no-prompt') @help('Use default answer to any prompt question (default is yes, except for destructive operations)') public noPrompt: boolean = false; @command @help('Abandon (cease processing) a specific app version that has not been published yet') public async abandon() { handleInterrupt(); if (!/[\w-]+@\d+\.\d+\.\d+(?:-\w+\.\d+)?/.test(this.appVersion)) { die('appVersion is required in the format of app@1.0.0'); } if (!await TerminalConfirm.ask( chalk.red('Abandoning is not reversible. Are you sure you want to abandon? [y/n]'), this.noPrompt, true)) { die('Operation cancelled.'); } const [appId, version] = this.appVersion.split('@'); console.log(chalk.gray(`Abandoning version ${this.appVersion} ...`)); try { const primaryAppVersion = await Rivendell.fetchAppVersion({ appId, version }, 'us'); let manifest; if (primaryAppVersion && primaryAppVersion.manifestJson) { manifest = JSON.parse(primaryAppVersion.manifestJson) as any; } const appVersionStates = new Map(); let shards = []; if (manifest && manifest.meta) { shards = await shardsFromManifest(manifest); } else { const allShards = (await Rivendell.shards()).map((s) => s.id); for (const shard of allShards) { try { const appVersion = await Rivendell.fetchAppVersion({ appId, version }, shard); if (appVersion) { appVersionStates.set(shard, appVersion.state); shards.push(shard); } } catch (error: any) { if (error instanceof ApiError && error.response?.status === httpConstants.HTTP_STATUS_NOT_FOUND) { continue; } die(`Could not locate the appVersion in region: ${shard}, error: ${formatError(error)}`); } } } let abandonable = false; for (const shard of shards) { try { let appVersionState; if (appVersionStates.get(shard)) { appVersionState = appVersionStates.get(shard); } else { const appVersion = await Rivendell.fetchAppVersion({ appId, version }, shard); appVersionState = appVersion.state; appVersionStates.set(shard, appVersionState); } if (ABANDONABLE_STATES.includes(appVersionState as AppVersionState)) { abandonable = true; } else if (appVersionState as AppVersionState !== AppVersionState.ABANDONED) { console.error(chalk.red(`AppVersion must be in a abandonable state in all regions. Exiting..`)); return; } } catch (error: any) { if (error instanceof ApiError && error.response?.status === httpConstants.HTTP_STATUS_NOT_FOUND) { console.error(chalk.red(`Could not locate the appVersion in region: ${shard}.`)); if (!await TerminalConfirm.ask('Continue with other regions?', this.noPrompt, true)) { die('Operation cancelled.'); } } else { throw error; } } } if (!abandonable) { console.log(chalk.yellow(`AppVersion is already in a abandoned state in all regions. Exiting..`)); return; } if (!shards.includes('us')) { try { await Rivendell.abandon(appId, version, 'us'); } catch (error: any) { console.error(chalk.red(`Failed to abandon in the primary region, error ${formatError(error)}`)); return; } } for (const shard of shards) { try { if (!appVersionStates.get(shard)) continue; const appVersionState = appVersionStates.get(shard); if (appVersionState as AppVersionState === AppVersionState.ABANDONED) { console.log(chalk.yellow(`Already abandoned in region ${shard}. Skipping...`)); continue; } await Rivendell.abandon(appId, version, shard); } catch (error: any) { die(`Failed to abandon in region ${shard}, error: ${formatError(error)}. Exiting...`); } } console.log(chalk.green('Abandon completed in all regions')); } catch (error: any) { console.error(chalk.red(`Error while abandoning ${formatError(error)}`)); } } }