@spotinst/spinnaker-deck
Version:
Spinnaker-Deck service, forked with support to Spotinst
197 lines (171 loc) • 7.39 kB
JavaScript
const fs = require('fs');
const path = require('path');
const prompts = require('prompts');
const kleur = require('kleur');
const ansi = require('sisteransi');
const yargs = require('yargs');
const { execSync } = require('child_process');
yargs.scriptName('publish.js').usage('$0 [args] [explicitPackage1...] [explicitPackage2...]').option('assert-master', {
type: 'boolean',
describe: 'turn off with --no-assert-master',
default: true,
});
const explicitPackages = yargs.argv._;
process.cwd(__dirname);
const p = (str) => process.stdout.write(str);
const files = fs.readdirSync(process.cwd());
const pkgs = files.filter((file) => fs.statSync(file).isDirectory() && fs.existsSync(path.join(file, 'package.json')));
// Ensure 'gh' is installed
try {
execSync('sh -c "which gh"');
} catch (error) {
console.error(`The github command line app was not found. Please install it using 'brew install github/gh/gh'`);
console.error(`See: https://cli.github.com/manual/installation`);
process.exit(1);
}
// Ensure user is using git:// protocol (not https) for upstream
const remotes = execSync('git remote -v')
.toString()
.split(/[\r\n]/);
const isHttpsUpstream = remotes.find((x) => x.includes('https://github.com/spinnaker/deck.git (push)'));
if (isHttpsUpstream) {
const upstream = /^([\w]+)/.exec(isHttpsUpstream)[1];
console.error(`The ${upstream} remote for spinnaker is using https protocol but should be using git`);
console.error(`Run the following command to correct this:`);
console.error();
console.error(`git remote set-url ${upstream} git@github.com:spinnaker/deck.git`);
process.exit(2);
}
const upstreamRemoteLine = remotes.find((x) => x.includes('git@github.com:spinnaker/deck.git (push)'));
const upstream = upstreamRemoteLine ? /^([\w]+)/.exec(upstreamRemoteLine)[1] : null;
// Ensure the working directory is clean and tracks master
try {
if (yargs.argv['assert-master']) {
execSync('sh -c "./assert_clean_master.sh"');
}
} catch (error) {
process.exit(3);
}
///////////////////////////////
// Fetch changelogs
///////////////////////////////
function status(message, index, total) {
p(ansi.erase.lines(3));
p(`${message}\n`);
p(kleur.blue(`[${'='.repeat(index)}${' '.repeat(total - index)}]`) + ` (${index}/${total})\n`);
}
p('\n\n');
status(`Fetching changelogs`, 0, pkgs.length);
const changelogs = pkgs.map((pkg, index) => {
status(`Fetching changelog for ${kleur.bold(pkg)}...`, index, pkgs.length);
return {
pkg,
lines: execSync(`/bin/sh -c './show_changelog.sh "${pkg}/package.json"'`)
.toString()
.split(/[\r\n]/)
.filter((str) => !!str),
};
});
status(`Fetched ${pkgs.length} changelogs`, pkgs.length, pkgs.length);
///////////////////////////////
// Ask user to choose packages
///////////////////////////////
const choices = changelogs.map(({ pkg, lines }) => {
const commitSummary = lines.length ? kleur.bold(`(${lines.length} unpublished commits)`) : kleur.dim('(not dirty)');
const useExplicitSelections = explicitPackages.length > 0;
return {
value: pkg,
selected: useExplicitSelections ? explicitPackages.includes(pkg) : lines.length > 0,
title: `${lines.length ? kleur.green(pkg) : kleur.dim(pkg)} ${commitSummary}`,
};
});
// Render a colorful changelog (above the menu) for the current item
function renderChangelog(changelog) {
const lineCount = changelog.lines.length;
const maxLineCount = changelogs.reduce((max, cl) => Math.max(max, cl.lines.length), 0);
const maxWidth = typeof process.stdout.getWindowSize === 'function' ? process.stdout.getWindowSize()[0] : 100;
const lines = changelog.lines
.map((str) => str.replace(/^([a-f0-9]{7})[a-f0-9]{33} /, '$1 ')) // show first 7 chars of hash
.map((str) => (str.length > maxWidth ? str.slice(0, maxWidth - 3) + kleur.dim('...') : str)) // truncate and ellipsis
.map((str) => str.replace(/^[a-f0-9]{7} /, (match) => kleur.green(match)))
.map((str) => str.replace(/(?:fix|chore|feat|docs|test|refactor)\([^)]*\): /, (match) => kleur.blue(match)))
.map((str) => str.replace(/\(#[0-9]+\)$/, (match) => kleur.green(match)));
// Add "blank" lines so the number of lines is consistent and the screen doesn't jump around
return '\n' + (lines.length ? lines.join('\n') + '\n' : '') + '#\n'.repeat(maxLineCount - lineCount);
}
const prompt = {
name: 'result',
type: 'multiselect',
message: 'Publish which packages?',
optionsPerPage: 15,
choices,
onRender: function () {
const changeLogForHighlighted = renderChangelog(changelogs[this.cursor]);
const selections = this.value
.filter((x) => x.selected)
.map((x) => kleur.green(x.value))
.join(', ');
this.instructions = `${selections}\n${changeLogForHighlighted}`;
},
};
(async () => {
const { result } = await prompts(prompt);
if (result) {
bumpPackages(result);
}
})();
///////////////////////////////
// Bump versions and create PR
///////////////////////////////
function bumpPackages(packages = []) {
let branchNameCreated = null;
const CHANGELOGTEMP = '___changelog.tmp.txt';
try {
const committer = execSync(`sh -c "git config --get user.name"`)
.toString()
.trim()
.toLocaleLowerCase()
.replace(/[^a-zA-Z]/g, '-');
const publishes = [];
// Update package.json and build the branch name
packages.forEach((pkg) => {
execSync(`sh -c "cd ${pkg}; npm version patch --no-git-tag-version"`);
const version = JSON.parse(fs.readFileSync(`${pkg}/package.json`).toString()).version;
const changelog = changelogs.find((cl) => cl.pkg === pkg);
publishes.push({ pkg, version, lines: changelog && changelog.lines });
});
const branchString = publishes.map((p) => `${p.pkg}-${p.version}`).join('-');
const branchName = `package-bump-${committer}-${branchString}`;
execSync(`sh -c "git checkout -b ${branchName}"`);
branchNameCreated = branchName;
publishes.forEach(({ pkg, version, lines }) => {
const commitMessage = `chore(${pkg}): publish ${pkg}@${version}\n\n\n${lines.join('\n')}`;
const commitMessageFile = `____commitmessage.${pkg}.tmp`;
try {
fs.writeFileSync(commitMessageFile, commitMessage);
execSync(`sh -c "git commit ${pkg}/package.json -F ${commitMessageFile}"`);
} finally {
fs.unlinkSync(commitMessageFile);
}
});
// Push to upstream, if configured
if (upstream) {
try {
execSync(`sh -c "git push ${upstream} ${branchName}"`);
} catch (error) {
// Probably failed because the user doesn't have write permission.
// This is OK: 'gh pr create' will create the PR in the user's fork
}
}
// export msg=$(cat msg) ; gh create pr -b "$msg"
const changes = publishes.map((p) => `## ${p.pkg}@${p.version}\n\n${p.lines.join('\n')}`);
fs.writeFileSync(CHANGELOGTEMP, changes.join('\n\n') + '\n\nPR created via `modules/publish.js`\n\n');
const title = 'chore(package): ' + publishes.map((p) => `${p.pkg}@${p.version}`).join(' ');
execSync(`sh -c 'export msg=$(cat "${CHANGELOGTEMP}") ; gh pr create --title "${title}" --body "$msg"'`);
} finally {
execSync(`sh -c "git checkout master"`);
branchNameCreated && execSync(`sh -c "git branch -D ${branchNameCreated}"`);
fs.unlinkSync(CHANGELOGTEMP);
}
}