Files
pwcli/src/commands/deployment/testservers/update/update.ts
T

387 lines
10 KiB
TypeScript

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();
}
},
};