Skip to content
Merged
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
110 changes: 110 additions & 0 deletions .github/scripts/add-labels-from-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* 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.
*/

module.exports = async ({ github, context, core }) => {
const commentBody = context.payload.comment.body;
const prNumber = context.payload.issue.number;

core.info(`Processing comment: ${commentBody}`);

// Parse comment for /cherry-pick branches
const cherryPickPattern = /^\/cherry-pick\s+(.+)$/m;
const match = commentBody.match(cherryPickPattern);

if (!match) {
core.warning('Comment does not match /cherry-pick pattern');
return { success: false, message: 'Invalid format' };
}

// Extract all release branches (space-separated)
const branchesText = match[1].trim();
const branchPattern = /release-\d+\.\d+(?:\.\d+)?/g;
const branches = branchesText.match(branchPattern) || [];

if (branches.length === 0) {
core.warning('No valid release branches found in comment');
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'confused'
});
return { success: false, message: 'No valid branches found' };
}

core.info(`Found branches: ${branches.join(', ')}`);

// Add labels to PR
const labels = branches.map(branch => `cherry-pick/${branch}`);

try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: labels
});
core.info(`Added labels: ${labels.join(', ')}`);
} catch (error) {
core.error(`Failed to add labels: ${error.message}`);
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: '-1'
});
return { success: false, message: error.message };
}

// React with checkmark emoji
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: '+1'
});

// Check if PR is already merged
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});

if (pullRequest.merged) {
core.info('PR is already merged - triggering backport immediately');

// Set branches in environment and trigger backport
process.env.BRANCHES_JSON = JSON.stringify(branches);

// Run backport script
const backportScript = require('./backport.js');
const results = await backportScript({ github, context, core });

return {
success: true,
message: `Labels added and backport triggered for: ${branches.join(', ')}`,
backportResults: results
};
} else {
core.info('PR not yet merged - labels added, backport will trigger on merge');
return {
success: true,
message: `Labels added for: ${branches.join(', ')}. Backport will trigger on merge.`
};
}
};

12 changes: 3 additions & 9 deletions .github/scripts/backport.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const { data: pullRequest } = await github.rest.pulls.get({

const prTitle = pullRequest.title;
const prAuthor = pullRequest.user.login;
const isMerged = pullRequest.merged;

// Get all commits from the PR
const { data: commits } = await github.rest.pulls.listCommits({
Expand Down Expand Up @@ -115,18 +114,13 @@ for (const targetBranch of branches) {
// Create pull request
const commitList = commits.map(c => `- \`${c.sha.substring(0, 7)}\` ${c.commit.message.split('\n')[0]}`).join('\n');

// Build PR body based on conflict status and merge status
// Build PR body based on conflict status
let prBody = `🤖 **Automated backport of #${prNumber} to \`${targetBranch}\`**\n\n`;

// Add merge status indicator
if (!isMerged) {
prBody += `⚠️ **Note:** The source PR #${prNumber} is not yet merged. This backport was created from the current state of the PR and may need updates if more commits are added before merge.\n\n`;
}

if (hasConflicts) {
prBody += `⚠️ **This PR has merge conflicts that need manual resolution.**

Original PR: #${prNumber} ${isMerged ? '(merged)' : '(not yet merged)'}
Original PR: #${prNumber}
Original Author: @${prAuthor}

**Cherry-picked commits (${commits.length}):**
Expand Down Expand Up @@ -154,7 +148,7 @@ git push --force-with-lease origin ${backportBranch}
} else {
prBody += `✅ Cherry-pick completed successfully with no conflicts.

Original PR: #${prNumber} ${isMerged ? '(merged)' : '(not yet merged)'}
Original PR: #${prNumber}
Original Author: @${prAuthor}

**Cherry-picked commits (${commits.length}):**
Expand Down
45 changes: 13 additions & 32 deletions .github/scripts/extract-branches.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,27 @@
module.exports = async ({ github, context, core }) => {
let branches = [];

// Get PR number
const prNumber = context.payload.pull_request?.number || context.payload.issue?.number;
// Get PR labels
const labels = context.payload.pull_request?.labels || [];

if (!prNumber) {
core.warning('Could not determine PR number from event - skipping backport');
if (labels.length === 0) {
core.info('No labels found on PR - skipping backport');
return [];
}

// Check PR body
if (context.payload.pull_request?.body) {
const prBody = context.payload.pull_request.body;
// Strict ASCII, anchored; allow X.Y or X.Y.Z
// Support multiple space-separated branches on one line
const lineMatches = prBody.matchAll(/^\/cherry-pick\s+(.+)$/gmi);
for (const match of lineMatches) {
const branchMatches = match[1].matchAll(/release-\d+\.\d+(?:\.\d+)?/g);
branches.push(...Array.from(branchMatches, m => m[0]));
// Extract branches from cherry-pick/* labels
const cherryPickPattern = /^cherry-pick\/(release-\d+\.\d+(?:\.\d+)?)$/;

for (const label of labels) {
const match = label.name.match(cherryPickPattern);
if (match) {
branches.push(match[1]);
core.info(`Found cherry-pick label: ${label.name} -> ${match[1]}`);
}
}

// Check all comments
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});

for (const comment of comments.data) {
const lineMatches = comment.body.matchAll(/^\/cherry-pick\s+(.+)$/gmi);
for (const match of lineMatches) {
const branchMatches = match[1].matchAll(/release-\d+\.\d+(?:\.\d+)?/g);
branches.push(...Array.from(branchMatches, m => m[0]));
}
}

// Deduplicate
branches = [...new Set(branches)];

if (branches.length === 0) {
core.info('No cherry-pick requests found - skipping backport');
core.info('No cherry-pick labels found - skipping backport');
return [];
}

Expand Down
38 changes: 35 additions & 3 deletions .github/workflows/cherrypick.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,51 @@ name: Cherry-Pick
on:
issue_comment:
types: [created]
pull_request:
types: [closed]

permissions:
contents: write
pull-requests: write
issues: write

jobs:
add-labels:
name: Add Cherry-Pick Labels from Comment
runs-on: ubuntu-latest
# Run on /cherry-pick comments on PRs
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/cherry-pick')

steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Configure git
run: |
git config user.name "nvidia-backport-bot"
git config user.email "[email protected]"

- name: Add labels and handle backport
uses: actions/github-script@v8
with:
script: |
const run = require('./.github/scripts/add-labels-from-comment.js');
return await run({ github, context, core });

backport:
name: Backport PR
runs-on: ubuntu-latest
# Run on /cherry-pick comments on PRs
# Run when PR is merged and has cherry-pick labels
if: |
github.event.issue.pull_request && startsWith(github.event.comment.body, '/cherry-pick')
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
contains(join(github.event.pull_request.labels.*.name, ','), 'cherry-pick/')

steps:
- name: Checkout repository
Expand All @@ -38,7 +70,7 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Extract target branches from PR comments
- name: Extract target branches from PR labels
id: extract-branches
uses: actions/github-script@v8
with:
Expand Down
Loading