387 lines
10 KiB
TypeScript
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();
|
|
}
|
|
},
|
|
};
|