import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; const chalk = require('chalk'); const { AutoComplete, Input, Confirm } = require('enquirer'); import { Octokit } from '@octokit/core'; import { exit } from 'process'; import { getTestStacksInfo } from '../../../../common/testservers'; import { defaultMenuSettings, getCurrentBranch, getSettings, sleep, } from '../../../../globals'; const { spawn } = require('child_process'); type Stack = { name: string; status: string; drift_status: string; branch: string; updated: string; }; const getBranchName = async ( toolbox: GluegunMenuToolbox, token: string, repoPath: string, ) => { const { print } = toolbox; const currentBranch = await getCurrentBranch(repoPath, toolbox); const input = new Input({ type: 'input', name: 'branch', message: 'What github branch should be deployed?', initial: currentBranch, }); const responseBranch = await input.run(); // First ask what branch should be deployed const octokit = new Octokit({ auth: token }); try { const response = await octokit.request( 'GET /repos/{owner}/{repo}/branches/{branch}', { owner: 'photowall', repo: 'photowall', branch: responseBranch, }, ); if (response.status === 200) { return responseBranch; } } catch (e) { print.error('Not a valid branch on photowall repo'); return null; } }; const getConfirmation = async (branchName: string, stackName: string) => { const confirm = new Confirm({ name: 'confirm', message: `The branch ${chalk.green( branchName, )} will deploy to ${chalk.yellow(stackName)}`, }); try { return confirm.run(); } catch (e) { console.error(e); return false; } }; module.exports = { name: 'update', alias: ['u'], description: 'Update stack with different branch (u)', hidden: false, run: async (toolbox: GluegunMenuToolbox) => { const { system, strings, print } = toolbox; // Check for parameters const jsonOutput = toolbox.parameters.options.json; const branchParam = toolbox.parameters.options.branch; const stackParam = toolbox.parameters.options.stack || toolbox.parameters.options.server; const skipConfirm = toolbox.parameters.options['skip-confirm']; // Load settings const config = await getSettings(toolbox); if (!config || !config.photowall_repo || config.photowall_repo === '') { if (jsonOutput) { console.log( JSON.stringify( { success: false, message: 'The update command requires that you set an absolute path to your local photowall repository.', }, null, 2, ), ); return; } print.error(` The create command requires that you set an absolute path to your local photowall repository. `); await toolbox.menu.showMenu('setup', defaultMenuSettings); return; } let branchName = null; // If branch is provided as parameter, validate it if (branchParam) { const octokit = new Octokit({ auth: config.gh_token }); try { const response = await octokit.request( 'GET /repos/{owner}/{repo}/branches/{branch}', { owner: 'photowall', repo: 'photowall', branch: branchParam, }, ); if (response.status === 200) { branchName = branchParam; } } catch (e) { if (jsonOutput) { console.log( JSON.stringify( { success: false, message: `Not a valid branch on photowall repo: ${branchParam}`, }, null, 2, ), ); return; } print.error(`Not a valid branch on photowall repo: ${branchParam}`); return; } } else { // Interactive mode - ask for branch while (!branchName) { branchName = await getBranchName( toolbox, config.gh_token, config.photowall_repo, ); } } const testStacks = await getTestStacksInfo(toolbox); let stackName = null; // If stack is provided as parameter, validate it if (stackParam) { const foundStack = testStacks.find((stack) => { return ( stack.name === stackParam || stack.name === `photowall-${stackParam}` || stack.name.replace('photowall-', '') === stackParam ); }); if (foundStack) { stackName = foundStack.name; } else { if (jsonOutput) { console.log( JSON.stringify( { success: false, message: `Stack not found: ${stackParam}`, availableStacks: testStacks.map((s) => s.name), }, null, 2, ), ); return; } print.error(`Stack not found: ${stackParam}`); print.info('Available stacks:'); testStacks.forEach((s) => print.info(` - ${s.name}`)); return; } } else { // Interactive mode - ask for stack const options = testStacks.map((item) => { return item.name; }); const prompt = new AutoComplete({ name: 'testserver', message: 'Choose a server', limit: options.length - 1, initial: options.length - 1, choices: options, }); stackName = await prompt.run(); } try { const stacksuffix = stackName.replace('photowall-', ''); // Skip confirmation if parameters are provided or skip-confirm flag is set const shouldRun = branchParam && stackParam ? skipConfirm ? true : await getConfirmation(branchName, stackName) : await getConfirmation(branchName, stackName); if (shouldRun) { const cmd = `make update-stack STACK_ENVIRONMENT_NAME=${stacksuffix} GITHUB_BRANCH=${branchName}`; if (!jsonOutput) { print.fancy( `Running [${chalk.yellow( cmd, )}] in folder [${`${config.photowall_repo}/cloudformation/`}]`, ); } const child = spawn( 'make', [ 'update-stack', `STACK_ENVIRONMENT_NAME=${stacksuffix}`, `GITHUB_BRANCH=${branchName}`, ], { cwd: `${config.photowall_repo}/cloudformation/`, }, ); child.stdout.setEncoding('utf8'); child.stdout.on('data', (data) => { if (!jsonOutput) { process.stdout.write(data); } }); child.stderr.on('data', (data) => { if (!jsonOutput) { process.stderr.write(data); } }); await new Promise((resolve, reject) => { child.on('close', (code) => { if (code === 0) { resolve(0); } else { reject(new Error(`make exited with code ${code}`)); } }); }); // Check the status const spinner = jsonOutput ? null : print.spin(`Waiting for stack to update`); let currentStackStatus = ''; while (currentStackStatus !== 'UPDATE_COMPLETE') { const stackCmd = `aws cloudformation describe-stacks --stack-name photowall-${stacksuffix}`; const response = await system.run(stackCmd, { trim: true }); const stackData = JSON.parse(response); currentStackStatus = stackData.Stacks[0].StackStatus; if (!jsonOutput) { let statusOutput = ''; switch (currentStackStatus) { case 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS': statusOutput = `[${chalk.yellow( currentStackStatus, )}]: Almost done, cleanup in progress`; break; case 'UPDATE_COMPLETE': statusOutput = `[${chalk.green( currentStackStatus, )}]: Update complete`; break; default: statusOutput = `[${chalk.red( currentStackStatus, )}]: Updating the stack`; break; } spinner.text = statusOutput; } if (currentStackStatus !== 'UPDATE_COMPLETE') { await sleep(1000); } } if (!jsonOutput) { spinner.text = `Will start the pipeline now [${chalk.yellow( `photowall-${stacksuffix}Pipeline`, )}]`; } const runPipelineCmd = `aws codepipeline start-pipeline-execution --name photowall-${stacksuffix}Pipeline`; await system.run(runPipelineCmd); await sleep(2000); const url = `https://test-${ stackName.split('photowall-test-')[1] }.photowall-test.se`; if (jsonOutput) { console.log( JSON.stringify( { success: true, message: `Successfully updated ${stackName} with branch ${branchName} and started pipeline`, stack: stackName, branch: branchName, pipeline: `photowall-${stacksuffix}Pipeline`, url, }, null, 2, ), ); } else { spinner.stop(); print.fancy('Pipeline started, bye! 🚀'); print.info(`Will be deployed soon on ${chalk.cyan(url)}`); } } else { // User cancelled if (jsonOutput) { console.log( JSON.stringify( { success: false, message: 'Update cancelled by user', }, null, 2, ), ); } else { print.info('Update cancelled'); } } } catch (e) { if (jsonOutput) { console.log( JSON.stringify( { success: false, message: `Error during update: ${e.message || e}`, error: e.message || String(e), }, null, 2, ), ); } else { console.log(`e`, e); } } // Skip menu if using parameters or JSON output if (jsonOutput || (branchParam && stackParam)) { return; } if (toolbox.fromMenu()) { await toolbox.menu.showMenu( 'deployment testservers', defaultMenuSettings, ); } else { exit(); } }, };