Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tools: add release-helper #51916

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Loading