-
Notifications
You must be signed in to change notification settings - Fork 394
Integrate bot for cherry-picking / backporting commits to release branches #1767
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
cb1895e
855aee7
e1ae960
87e9703
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,222 @@ | ||||||
module.exports = async ({ github, context, core }) => { | ||||||
const branches = JSON.parse(process.env.BRANCHES_JSON || '[]'); | ||||||
|
||||||
// Get PR number from event | ||||||
const prNumber = context.payload.pull_request?.number || context.payload.issue.number; | ||||||
|
||||||
// Fetch full PR data (needed when triggered via issue_comment) | ||||||
const { data: pullRequest } = await github.rest.pulls.get({ | ||||||
owner: context.repo.owner, | ||||||
repo: context.repo.repo, | ||||||
pull_number: prNumber | ||||||
}); | ||||||
|
||||||
const prTitle = pullRequest.title; | ||||||
const prAuthor = pullRequest.user.login; | ||||||
|
||||||
// Validate PR is merged | ||||||
if (!pullRequest.merged) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I was thinking if we really needed to block the cherrypick PR creation on the merge of the original. Why can't we just create the cherrypick PR regardless of merge status? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main pro of blocking are that cherry-picks are based on reviewed, approved, and merged code. Therefore, cherry-picks represents the "final" state of the changes. I also thought it was cleaner in case a PR is abandoned or closed without merging. |
||||||
// If triggered by a comment on an unmerged PR, acknowledge and exit gracefully | ||||||
if (context.eventName === 'issue_comment') { | ||||||
core.info('PR is not merged yet. Acknowledging /cherry-pick command and will backport after merge.'); | ||||||
// Add a reaction to the comment | ||||||
await github.rest.reactions.createForIssueComment({ | ||||||
owner: context.repo.owner, | ||||||
repo: context.repo.repo, | ||||||
comment_id: context.payload.comment.id, | ||||||
content: 'eyes' | ||||||
}); | ||||||
// Add a comment explaining what will happen | ||||||
await github.rest.issues.createComment({ | ||||||
owner: context.repo.owner, | ||||||
repo: context.repo.repo, | ||||||
issue_number: prNumber, | ||||||
body: `👀 Backport request acknowledged for: ${branches.map(b => `\`${b}\``).join(', ')}\n\nThe backport PR(s) will be automatically created when this PR is merged.` | ||||||
}); | ||||||
return []; | ||||||
} | ||||||
core.setFailed('PR is not merged yet. Only merged PRs can be backported.'); | ||||||
return; | ||||||
} | ||||||
|
||||||
// Get all commits from the PR | ||||||
const { data: commits } = await github.rest.pulls.listCommits({ | ||||||
owner: context.repo.owner, | ||||||
repo: context.repo.repo, | ||||||
pull_number: prNumber | ||||||
}); | ||||||
|
||||||
if (commits.length === 0) { | ||||||
core.setFailed('No commits found in PR. This should not happen for merged PRs.'); | ||||||
return; | ||||||
} | ||||||
|
||||||
core.info(`Backporting PR #${prNumber}: "${prTitle}"`); | ||||||
core.info(`Commits to cherry-pick: ${commits.length}`); | ||||||
commits.forEach((commit, index) => { | ||||||
core.info(` ${index + 1}. ${commit.sha.substring(0, 7)} - ${commit.commit.message.split('\n')[0]}`); | ||||||
}); | ||||||
|
||||||
const { execSync } = require('child_process'); | ||||||
|
||||||
const results = []; | ||||||
|
||||||
for (const targetBranch of branches) { | ||||||
core.info(`\n========================================`); | ||||||
core.info(`Backporting to ${targetBranch}`); | ||||||
core.info(`========================================`); | ||||||
const backportBranch = `backport-${prNumber}-to-${targetBranch}`; | ||||||
try { | ||||||
// Create backport branch from target release branch | ||||||
core.info(`Creating branch ${backportBranch} from ${targetBranch}`); | ||||||
execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { stdio: 'inherit' }); | ||||||
execSync(`git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' }); | ||||||
|
execSync(`git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' }); | |
execSync(`git checkout ${backportBranch} || git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' });``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Resolving the conflicts will most likely require a force push:
4. Push the resolution: \`git push origin ${backportBranch}\` | |
4. Push the resolution: \`git push -f origin ${backportBranch}\` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I understand that this may be idiomatic, but looking for this :
to differentiate between the two contents is not ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double end of file line
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,42 @@ | ||||||||
module.exports = async ({ github, context, core }) => { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Licence header There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. |
||||||||
let branches = []; | ||||||||
|
||||||||
// Get PR number | ||||||||
const prNumber = context.payload.pull_request?.number || context.payload.issue?.number; | ||||||||
|
||||||||
if (!prNumber) { | ||||||||
core.setFailed('Could not determine PR number from event'); | ||||||||
return []; | ||||||||
} | ||||||||
|
||||||||
// Check PR body | ||||||||
if (context.payload.pull_request?.body) { | ||||||||
const prBody = context.payload.pull_request.body; | ||||||||
// Enforce release-X.Y or release-X.Y.Z | ||||||||
const bodyMatches = prBody.matchAll(/\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/g); | ||||||||
|
const bodyMatches = prBody.matchAll(/\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/g); | |
// Strict ASCII, anchored; allow X.Y or X.Y.Z | |
const bodyMatches = prBody.matchAll(/^\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/gmi); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,67 @@ | ||||||
# Copyright 2025 NVIDIA CORPORATION | ||||||
# | ||||||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
# you may not use this file except in compliance with the License. | ||||||
# You may obtain a copy of the License at | ||||||
# | ||||||
# http://www.apache.org/licenses/LICENSE-2.0 | ||||||
# | ||||||
# Unless required by applicable law or agreed to in writing, software | ||||||
# distributed under the License is distributed on an "AS IS" BASIS, | ||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
# See the License for the specific language governing permissions and | ||||||
# limitations under the License. | ||||||
|
||||||
name: Cherry-Pick | ||||||
|
||||||
on: | ||||||
pull_request_target: | ||||||
types: [closed] | ||||||
issue_comment: | ||||||
types: [created] | ||||||
|
||||||
permissions: | ||||||
contents: write | ||||||
pull-requests: write | ||||||
issues: write | ||||||
|
||||||
jobs: | ||||||
backport: | ||||||
name: Backport PR | ||||||
runs-on: ubuntu-latest | ||||||
# Run on merged PRs OR on /cherry-pick comments | ||||||
if: | | ||||||
(github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) || | ||||||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/cherry-pick')) | ||||||
|
||||||
steps: | ||||||
- name: Checkout repository | ||||||
uses: actions/checkout@v4 | ||||||
with: | ||||||
fetch-depth: 0 | ||||||
token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|
||||||
- name: Extract target branches from PR comments | ||||||
id: extract-branches | ||||||
uses: actions/github-script@v7 | ||||||
with: | ||||||
script: | | ||||||
const run = require('./.github/scripts/extract-branches.js'); | ||||||
return await run({ github, context, core }); | ||||||
|
||||||
- name: Configure git | ||||||
run: | | ||||||
git config user.name "nvidia-backport-bot" | ||||||
git config user.email "noreply@nvidia.com" | ||||||
|
||||||
- name: Backport to release branches | ||||||
id: backport | ||||||
uses: actions/github-script@v7 | ||||||
|
uses: actions/github-script@v7 | |
uses: actions/github-script@v8 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
tariq1890 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double end of file line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a Licence Header
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.