Created
March 10, 2022 23:25
-
-
Save bdombro/bfb2dc90f2949f60296d9f46ceed1ac3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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