Skip to content

Instantly share code, notes, and snippets.

@bdombro
Created March 10, 2022 23:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bdombro/bfb2dc90f2949f60296d9f46ceed1ac3 to your computer and use it in GitHub Desktop.
Save bdombro/bfb2dc90f2949f60296d9f46ceed1ac3 to your computer and use it in GitHub Desktop.
#!/usr/bin/env zx
$.verbose = argv.verbose;
$.silent = argv.silent;
import {
assertGit,
assertGithubCli,
getGitCtxT,
HandledError,
log,
throwHandledError,
} from './zx-lib.mjs';
const about = `
/**
* pr-eager-merge
*
* This script automates the process of merging+closing a PR:
* - checking if the PR is ready to merge (mergeable and approved), bailing if not
* - watching the status: rebasing if behind master, tingle results
* - merging+closing the PR if all checks pass
*/
npx zx scripts/pr-eager-merge.mjs [--automerge] [--allLocalBranches] [--verbose] [--silent]
`;
export default async function main(props = {}) {
const { automerge, allLocalBranches } = parseProps(props);
try {
await log.announce('HELLO EAGER MERGER');
await log.info('props: ' + JSON.stringify(props));
await assertGit();
await assertGithubCli();
if (allLocalBranches) {
await mergeAllLocalBranches({ automerge });
} else {
await mergeCurrentBranch({ automerge });
}
log.newLine();
log.announce('DONE');
} catch (e) {
await log.newLine(); // end the last line
if (e instanceof HandledError) {
await log.error(e.message);
await log.say(e.message);
} else {
await log.say('error merge failed');
throw e;
}
}
}
async function mergeAllLocalBranches({ automerge }) {
log.announce(`MERGING ALL LOCAL BRANCHES`);
const branches = (await $`git branch`).stdout
.split('\n')
.map(l => l.trim())
.filter(Boolean)
.filter(l => !l.startsWith('*'));
for (const b of branches) {
await $`git checkout ${b}`;
await mergeCurrentBranch({ automerge });
}
log.announce('mergeAllLocalBranches:DONE');
}
async function mergeCurrentBranch({ automerge }) {
let ctx = await getGitCtxT(true);
let waitCount = 0;
if (ctx.isLocalOnly) {
throwHandledError('branch not on remote.');
}
if (ctx.state === 'MERGED') {
throwHandledError('PR is already merged.');
}
if (ctx.closed) {
throwHandledError('PR is already closed.');
}
if (ctx.mergeable !== 'MERGEABLE') {
throwHandledError('PR is not mergeable.');
}
if (automerge) {
await $`gh pr merge -d --auto --squash`;
}
await log.announce(`MERGING ${ctx.branch}`);
while ((ctx = await getGitCtxT(true)).state !== 'MERGED' ) {
if (ctx.closed) {
throwHandledError('PR is strangely closed but not merged.');
}
if (ctx.branch === 'master') {
throwHandledError('on master branch!');
}
if (ctx.reviewDecision !== 'APPROVED') {
throwHandledError('PR is not approved yet.');
}
if (!['MERGEABLE', 'UNKNOWN'].includes(ctx.mergeable)) {
throwHandledError('PR is not mergeable.');
}
if (ctx.isBehind) {
await log.info('behind master, rebasing');
if (waitCount) {
// Reset waitCount and end last line
await log.newLine();
waitCount = 0;
}
await $`git rebase origin/master --autostash`;
await $`git push -f`;
continue;
}
if (ctx.checks.failure) throw new HandledError(`${ctx.checks.failure} checks failed`);
if (ctx.checks.pending || ctx.mergeable === 'UNKNOWN') {
await log.info(`waiting for pending tests: ${waitCount++}s`, waitCount);
await sleep(1e3);
continue;
}
}
if (automerge) {
await $`git checkout master`;
await $`git branch -D ${ctx.branch}`;
} else {
await log.info('all checks passed! merging');
try {
await $`gh pr merge -d --squash`;
} catch (e) {
await $`gh pr merge -d --merge`;
}
}
await $`git pull`;
await log.newLine(); // end the last line
await log.say('PR is closed');
}
function parseProps(props) {
const { _, automerge = true, allLocalBranches, verbose, silent, ...rest } = props;
if (Object.keys(rest).length) {
exitWithHelp();
}
return { automerge, allLocalBranches, verbose, silent };
}
function exitWithHelp() {
log(about);
process.exit(1);
}
// If script is being called directly, run it
const isCalled =
process.argv[2].split('/').pop() === import.meta.url.split('/').pop();
if (isCalled) {
$.verbose = argv.verbose;
$.silent = argv.silent;
if (!globalThis.$) {
throwError('This script must be ran using zx (https://github.com/google/zx)');
}
await main(argv);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment