Skip to content

Commit

Permalink
tools: add release-helper
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ruyadorno committed Feb 28, 2024
1 parent 399654f commit e8b925b
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 0 deletions.
30 changes: 30 additions & 0 deletions tools/release/backport.js
Original file line number Diff line number Diff line change
@@ -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();
91 changes: 91 additions & 0 deletions tools/release/helper.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
Usage: release-helper <cmd>
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
99 changes: 99 additions & 0 deletions tools/release/watch-cherry-pick.js
Original file line number Diff line number Diff line change
@@ -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(/^\ \ \ \ (?<title>\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();

0 comments on commit e8b925b

Please sign in to comment.