Skip to content

Commit 38d2ce9

Browse files
authored
Merge pull request #1378 from elezar/add-cherry-pick-automation
[no-relnote] Update cherrypick workflow from gpu-operator repo
2 parents b1e4c3b + 33e13ed commit 38d2ce9

File tree

4 files changed

+161
-44
lines changed

4 files changed

+161
-44
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright 2025 NVIDIA CORPORATION
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
module.exports = async ({ github, context, core }) => {
18+
const commentBody = context.payload.comment.body;
19+
const prNumber = context.payload.issue.number;
20+
21+
core.info(`Processing comment: ${commentBody}`);
22+
23+
// Parse comment for /cherry-pick branches
24+
const cherryPickPattern = /^\/cherry-pick\s+(.+)$/m;
25+
const match = commentBody.match(cherryPickPattern);
26+
27+
if (!match) {
28+
core.warning('Comment does not match /cherry-pick pattern');
29+
return { success: false, message: 'Invalid format' };
30+
}
31+
32+
// Extract all release branches (space-separated)
33+
const branchesText = match[1].trim();
34+
const branchPattern = /release-\d+\.\d+(?:\.\d+)?/g;
35+
const branches = branchesText.match(branchPattern) || [];
36+
37+
if (branches.length === 0) {
38+
core.warning('No valid release branches found in comment');
39+
await github.rest.reactions.createForIssueComment({
40+
owner: context.repo.owner,
41+
repo: context.repo.repo,
42+
comment_id: context.payload.comment.id,
43+
content: 'confused'
44+
});
45+
return { success: false, message: 'No valid branches found' };
46+
}
47+
48+
core.info(`Found branches: ${branches.join(', ')}`);
49+
50+
// Add labels to PR
51+
const labels = branches.map(branch => `cherry-pick/${branch}`);
52+
53+
try {
54+
await github.rest.issues.addLabels({
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
issue_number: prNumber,
58+
labels: labels
59+
});
60+
core.info(`Added labels: ${labels.join(', ')}`);
61+
} catch (error) {
62+
core.error(`Failed to add labels: ${error.message}`);
63+
await github.rest.reactions.createForIssueComment({
64+
owner: context.repo.owner,
65+
repo: context.repo.repo,
66+
comment_id: context.payload.comment.id,
67+
content: '-1'
68+
});
69+
return { success: false, message: error.message };
70+
}
71+
72+
// React with checkmark emoji
73+
await github.rest.reactions.createForIssueComment({
74+
owner: context.repo.owner,
75+
repo: context.repo.repo,
76+
comment_id: context.payload.comment.id,
77+
content: '+1'
78+
});
79+
80+
// Check if PR is already merged
81+
const { data: pullRequest } = await github.rest.pulls.get({
82+
owner: context.repo.owner,
83+
repo: context.repo.repo,
84+
pull_number: prNumber
85+
});
86+
87+
if (pullRequest.merged) {
88+
core.info('PR is already merged - triggering backport immediately');
89+
90+
// Set branches in environment and trigger backport
91+
process.env.BRANCHES_JSON = JSON.stringify(branches);
92+
93+
// Run backport script
94+
const backportScript = require('./backport.js');
95+
const results = await backportScript({ github, context, core });
96+
97+
return {
98+
success: true,
99+
message: `Labels added and backport triggered for: ${branches.join(', ')}`,
100+
backportResults: results
101+
};
102+
} else {
103+
core.info('PR not yet merged - labels added, backport will trigger on merge');
104+
return {
105+
success: true,
106+
message: `Labels added for: ${branches.join(', ')}. Backport will trigger on merge.`
107+
};
108+
}
109+
};
110+

.github/scripts/backport.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const { data: pullRequest } = await github.rest.pulls.get({
2929

3030
const prTitle = pullRequest.title;
3131
const prAuthor = pullRequest.user.login;
32-
const isMerged = pullRequest.merged;
3332

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

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

121-
// Add merge status indicator
122-
if (!isMerged) {
123-
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`;
124-
}
125-
126120
if (hasConflicts) {
127121
prBody += `⚠️ **This PR has merge conflicts that need manual resolution.**
128122
129-
Original PR: #${prNumber} ${isMerged ? '(merged)' : '(not yet merged)'}
123+
Original PR: #${prNumber}
130124
Original Author: @${prAuthor}
131125
132126
**Cherry-picked commits (${commits.length}):**
@@ -154,7 +148,7 @@ git push --force-with-lease origin ${backportBranch}
154148
} else {
155149
prBody += `✅ Cherry-pick completed successfully with no conflicts.
156150
157-
Original PR: #${prNumber} ${isMerged ? '(merged)' : '(not yet merged)'}
151+
Original PR: #${prNumber}
158152
Original Author: @${prAuthor}
159153
160154
**Cherry-picked commits (${commits.length}):**

.github/scripts/extract-branches.js

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,27 @@
1717
module.exports = async ({ github, context, core }) => {
1818
let branches = [];
1919

20-
// Get PR number
21-
const prNumber = context.payload.pull_request?.number || context.payload.issue?.number;
20+
// Get PR labels
21+
const labels = context.payload.pull_request?.labels || [];
2222

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

28-
// Check PR body
29-
if (context.payload.pull_request?.body) {
30-
const prBody = context.payload.pull_request.body;
31-
// Strict ASCII, anchored; allow X.Y or X.Y.Z
32-
// Support multiple space-separated branches on one line
33-
const lineMatches = prBody.matchAll(/^\/cherry-pick\s+(.+)$/gmi);
34-
for (const match of lineMatches) {
35-
const branchMatches = match[1].matchAll(/release-\d+\.\d+(?:\.\d+)?/g);
36-
branches.push(...Array.from(branchMatches, m => m[0]));
28+
// Extract branches from cherry-pick/* labels
29+
const cherryPickPattern = /^cherry-pick\/(release-\d+\.\d+(?:\.\d+)?)$/;
30+
31+
for (const label of labels) {
32+
const match = label.name.match(cherryPickPattern);
33+
if (match) {
34+
branches.push(match[1]);
35+
core.info(`Found cherry-pick label: ${label.name} -> ${match[1]}`);
3736
}
3837
}
3938

40-
// Check all comments
41-
const comments = await github.rest.issues.listComments({
42-
owner: context.repo.owner,
43-
repo: context.repo.repo,
44-
issue_number: prNumber
45-
});
46-
47-
for (const comment of comments.data) {
48-
const lineMatches = comment.body.matchAll(/^\/cherry-pick\s+(.+)$/gmi);
49-
for (const match of lineMatches) {
50-
const branchMatches = match[1].matchAll(/release-\d+\.\d+(?:\.\d+)?/g);
51-
branches.push(...Array.from(branchMatches, m => m[0]));
52-
}
53-
}
54-
55-
// Deduplicate
56-
branches = [...new Set(branches)];
57-
5839
if (branches.length === 0) {
59-
core.info('No cherry-pick requests found - skipping backport');
40+
core.info('No cherry-pick labels found - skipping backport');
6041
return [];
6142
}
6243

.github/workflows/cherrypick.yml

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,51 @@ name: Cherry-Pick
1717
on:
1818
issue_comment:
1919
types: [created]
20+
pull_request:
21+
types: [closed]
2022

2123
permissions:
2224
contents: write
2325
pull-requests: write
2426
issues: write
2527

2628
jobs:
29+
add-labels:
30+
name: Add Cherry-Pick Labels from Comment
31+
runs-on: ubuntu-latest
32+
# Run on /cherry-pick comments on PRs
33+
if: |
34+
github.event_name == 'issue_comment' &&
35+
github.event.issue.pull_request &&
36+
startsWith(github.event.comment.body, '/cherry-pick')
37+
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@v5
41+
with:
42+
fetch-depth: 0
43+
token: ${{ secrets.GITHUB_TOKEN }}
44+
45+
- name: Configure git
46+
run: |
47+
git config user.name "nvidia-backport-bot"
48+
git config user.email "[email protected]"
49+
50+
- name: Add labels and handle backport
51+
uses: actions/github-script@v8
52+
with:
53+
script: |
54+
const run = require('./.github/scripts/add-labels-from-comment.js');
55+
return await run({ github, context, core });
56+
2757
backport:
2858
name: Backport PR
2959
runs-on: ubuntu-latest
30-
# Run on /cherry-pick comments on PRs
60+
# Run when PR is merged and has cherry-pick labels
3161
if: |
32-
github.event.issue.pull_request && startsWith(github.event.comment.body, '/cherry-pick')
62+
github.event_name == 'pull_request' &&
63+
github.event.pull_request.merged == true &&
64+
contains(join(github.event.pull_request.labels.*.name, ','), 'cherry-pick/')
3365
3466
steps:
3567
- name: Checkout repository
@@ -38,7 +70,7 @@ jobs:
3870
fetch-depth: 0
3971
token: ${{ secrets.GITHUB_TOKEN }}
4072

41-
- name: Extract target branches from PR comments
73+
- name: Extract target branches from PR labels
4274
id: extract-branches
4375
uses: actions/github-script@v8
4476
with:

0 commit comments

Comments
 (0)