From e8b925b70a530c5e7344d6281d77c0da2e634b96 Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Wed, 28 Feb 2024 17:43:40 -0500 Subject: [PATCH] tools: add release-helper Adds a `release-helper` script that orchestrates the multiple tools used during the cherry picking steps when working on a release line branch. Aiming to simplify and speed up the manual cherry-pick / commit review process of each release. --- tools/release/backport.js | 30 +++++++++ tools/release/helper.sh | 91 +++++++++++++++++++++++++++ tools/release/watch-cherry-pick.js | 99 ++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 tools/release/backport.js create mode 100755 tools/release/helper.sh create mode 100644 tools/release/watch-cherry-pick.js diff --git a/tools/release/backport.js b/tools/release/backport.js new file mode 100644 index 00000000000000..93dd44659a04d3 --- /dev/null +++ b/tools/release/backport.js @@ -0,0 +1,30 @@ +const {execSync} = require('child_process') +const {readFileSync, writeFileSync} = require('fs') + +const CONFLICT_INFO_FILENAME = '.cherry-pick-conflict-info.json' + +async function main () { + const cherryPickConflictInfoJson = readFileSync(CONFLICT_INFO_FILENAME, { encoding: 'utf8' }) + const backportCommitInfo = JSON.parse(cherryPickConflictInfoJson).backport + + if (backportCommitInfo) { + const {id, labelName, msg, sha, url} = backportCommitInfo + + const ghOpts = { + encoding: 'utf8', + stdio: 'inherit', + } + execSync(`gh pr comment ${id} --body '${msg}'`, ghOpts) + execSync(`gh pr edit ${id} --add-label '${labelName}'`, ghOpts) + + console.log('REQUESTED BACKPORT:') + console.log(sha, url) + writeFileSync(CONFLICT_INFO_FILENAME, JSON.stringify({ + backport: null + }, null, 2)) + } else { + console.error(`No backport commit info found in ${CONFLICT_INFO_FILENAME}`) + } +} + +main(); diff --git a/tools/release/helper.sh b/tools/release/helper.sh new file mode 100755 index 00000000000000..b9e56655625201 --- /dev/null +++ b/tools/release/helper.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# Requirements: +# The release-helper script requires having branch-diff, changelog-maker and +# the GitHub CLI installed and properly authorized in the current machine. +if [[ -z "${NODEJS_RELEASE_LINE}" ]]; then + printf '%s\n' "NODEJS_RELEASE_LINE env var needs to be defined." >&2 + exit 1 +else + CURRENT="${NODEJS_RELEASE_LINE}" +fi + +# startcherrypick will paginate and parse through the list of commits in +# chunks of 20, so that it's more convenient for releasers to work in batches +# having time to update the staging branch in between working sessions. +function startcherrypick() { + head -n 20 .commit_list | xargs git cherry-pick 2>&1 | node ./tools/release/watch-cherry-pick.js + tail -n +21 .commit_list > .commit_list_next + mv .commit_list_next .commit_list +} + +function endcherrypick() { + rm .commit_list +} + +function helpmsg() { + cat < + +To start working on a new release begin with either of the startup commands: + +release-helper cherry-pick +OR +release-helper prepare + + +Commands: +release-helper cherry-pick Starts cherry-picking from branch-diff without using a local cache file +release-helper prepare Uses branch-diff to cache a local file with a list of commits to work with +release-helper start Starts cherry picking commits from main into the release line branch +release-helper backport During cherry pick, asks original PR author for a backport using gh cli +release-helper skip During cherry pick, skips a commit that has conflicts +release-helper continue During cherry pick, after amending a commit resume the cherry picking +release-helper end Stop cherry picking commits +release-helper notable Retrieves notable changes, requires being in the proposal branch +release-helper notable-md Markdown notable changes, requires being in the proposal branch +release-helper changelog When in a proposal branch generates the changelog using changelog-maker +EOF +} + +case $1 in + help|--help|-h) + helpmsg + ;; + changelog) + changelog-maker --start-ref=$1 --group --filter-release --markdown + ;; + notable) + branch-diff upstream/$CURRENT.x $(git cb) --require-label=notable-change --plaintext + ;; + notable-md) + branch-diff upstream/$CURRENT.x $(git cb) --require-label=notable-change --markdown + ;; + cherry-pick) + branch-diff $CURRENT.x-staging upstream/main --exclude-label=semver-major,dont-land-on-$CURRENT.x,backport-requested-$CURRENT.x,backported-to-$CURRENT.x,backport-blocked-$CURRENT.x,backport-open-$CURRENT.x --filter-release --format=sha --reverse --cache | head | xargs git cherry-pick 2>&1 | node ./tools/release/watch-cherry-pick.js + ;; + start) + startcherrypick + ;; + end) + endcherrypick; git cherry-pick --quit + ;; +# prepare will run branch-diff only once and store the retrieved metadata in +# a `.commit_list` file, which is a pratical way to avoid hitting GitHub API +# rate limits. + prepare) + branch-diff $CURRENT.x-staging upstream/main --exclude-label=semver-major,dont-land-on-$CURRENT.x,backport-requested-$CURRENT.x,backported-to-$CURRENT.x,backport-blocked-$CURRENT.x,backport-open-$CURRENT.x --filter-release --format=sha --reverse > .commit_list + ;; + backport) + node ./tools/release/backport.js + ;; + continue) + git -c core.editor=true cherry-pick --continue 2>&1 | node ./tools/release/watch-cherry-pick.js + ;; + skip) + git cherry-pick --skip 2>&1 | node ./tools/release/watch-cherry-pick.js + ;; + * ) + helpmsg + ;; +esac diff --git a/tools/release/watch-cherry-pick.js b/tools/release/watch-cherry-pick.js new file mode 100644 index 00000000000000..3d778ebaa7793b --- /dev/null +++ b/tools/release/watch-cherry-pick.js @@ -0,0 +1,99 @@ +const {execSync} = require('child_process') +const {writeFileSync} = require('fs') + +const LOG_FILE = '.watch-cherry-pick.log' +const CONFLICT_INFO_FILENAME = '.cherry-pick-conflict-info.json' +const CURRENT_RELEASE = process.env.NODEJS_RELEASE_LINE +const BRANCH_NAME = `${CURRENT_RELEASE}.x-staging` +const msg = 'This commit does not land cleanly on `' + BRANCH_NAME + +'` and will need manual backport in case we want it in **' + +CURRENT_RELEASE + '**.' +const labelName = `backport-requested-${CURRENT_RELEASE}.x` + +function getCommitTitle(body) { + const re = body.match(/^\ \ \ \ (?\w*\:.*$)/m) + if (re && re.groups) { + return re.groups.title + } +} + +function getInfoFromCommit(sha) { + const body = execSync(`git show -s ${sha}`, { encoding: 'utf8' }) + const title = getCommitTitle(body) + const url = body.match(/^.*(PR-URL:).?(?<url>.*)/im).groups.url + const [id] = url.split('/').slice(-1) + const labelsJson = execSync(`gh pr view ${id} --json=labels`, { encoding: 'utf8' }) + const labels = JSON.parse(labelsJson).labels.map(i => i.name) + return { sha, title, url, id, msg, labelName, labels, body } +} + +function getConflictCommitMsg(commitInfo) { + const {body, labels} = commitInfo + return `CONFLICT APPLYING COMMIT: +${body} +labels: ${labels} +` +} + +function getSuccessCommitSha(cherryPickResult) { + const re = cherryPickResult.match(/^\[v20\.x\-staging\ (?<sha>\b[0-9a-f]{7,40}\b)\]/) + if (re && re.groups) { + return re.groups.sha + } +} + +function getConflictCommitSha(cherryPickResult) { + const re = cherryPickResult.match(/^error\:.*\ (?<sha>\b[0-9a-f]{7,40}\b)\.\.\./m) + if (re && re.groups) { + return re.groups.sha + } +} + +const pickedCommits = [] +let conflictCommitInfo; +let conflictCommitMsg; +async function main() { + for await (const data of process.stdin) { + const cherryPickResult = String(data) + + writeFileSync(LOG_FILE, cherryPickResult, { flag: 'a' }) + + // handles commits that were successfully picked + let sha = getSuccessCommitSha(cherryPickResult) + if (sha) { + pickedCommits.push(sha) + } else { + // handles a current conflict that needs manual action + sha = getConflictCommitSha(cherryPickResult) + if (sha) { + conflictCommitInfo = getInfoFromCommit(sha) + conflictCommitMsg = getConflictCommitMsg(conflictCommitInfo) + } + } + } + + if (pickedCommits.length) { + console.log('SUCCESSFULLY PICKED COMMITS REPORT') + console.log('---') + pickedCommits + .map(i => getInfoFromCommit(i)) + .forEach(({ sha, title, url, labels }) => { + console.log(sha, url) + console.log(title) + console.log('labels:', labels.join(', ')) + console.log('---') + }) + } else { + console.log('NO ADDITIONAL COMMIT PICKED') + } + console.log('\n') + + if (conflictCommitInfo) { + console.error(conflictCommitMsg) + writeFileSync(CONFLICT_INFO_FILENAME, JSON.stringify({ + backport: conflictCommitInfo + }, null, 2)) + } +} + +main();