Skip to content

Commit 6f3d599

Browse files
authored
Merge pull request #1812 from karthikvetrivel/ci/integrate-cherry-pick-bot
fix: allow workflow to run with no branches identified + support PR updates
2 parents dc6d485 + 316284b commit 6f3d599

File tree

3 files changed

+142
-78
lines changed

3 files changed

+142
-78
lines changed

.github/scripts/backport.js

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

3030
const prTitle = pullRequest.title;
3131
const prAuthor = pullRequest.user.login;
32-
33-
// Validate PR is merged
34-
if (!pullRequest.merged) {
35-
// If triggered by a comment on an unmerged PR, acknowledge and exit gracefully
36-
if (context.eventName === 'issue_comment') {
37-
core.info('PR is not merged yet. Acknowledging /cherry-pick command and will backport after merge.');
38-
// Add a reaction to the 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: 'eyes'
44-
});
45-
// Add a comment explaining what will happen
46-
await github.rest.issues.createComment({
47-
owner: context.repo.owner,
48-
repo: context.repo.repo,
49-
issue_number: prNumber,
50-
body: `👀 Backport request acknowledged for: ${branches.map(b => `\`${b}\``).join(', ')}\n\nThe backport PR(s) will be automatically created when this PR is merged.`
51-
});
52-
return [];
53-
}
54-
core.setFailed('PR is not merged yet. Only merged PRs can be backported.');
55-
return;
56-
}
32+
const isMerged = pullRequest.merged;
5733

5834
// Get all commits from the PR
5935
const { data: commits } = await github.rest.pulls.listCommits({
@@ -63,8 +39,8 @@ const { data: commits } = await github.rest.pulls.listCommits({
6339
});
6440

6541
if (commits.length === 0) {
66-
core.setFailed('No commits found in PR. This should not happen for merged PRs.');
67-
return;
42+
core.warning('No commits found in PR - skipping backport');
43+
return [];
6844
}
6945

7046
core.info(`Backporting PR #${prNumber}: "${prTitle}"`);
@@ -83,10 +59,10 @@ for (const targetBranch of branches) {
8359
core.info(`========================================`);
8460
const backportBranch = `backport-${prNumber}-to-${targetBranch}`;
8561
try {
86-
// Create backport branch from target release branch
87-
core.info(`Creating branch ${backportBranch} from ${targetBranch}`);
62+
// Create/reset backport branch from target release branch
63+
core.info(`Creating/resetting branch ${backportBranch} from ${targetBranch}`);
8864
execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { stdio: 'inherit' });
89-
execSync(`git checkout ${backportBranch} || git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' });
65+
execSync(`git checkout -B ${backportBranch} ${targetBranch}`, { stdio: 'inherit' });
9066
// Cherry-pick each commit from the PR
9167
let hasConflicts = false;
9268
for (let i = 0; i < commits.length; i++) {
@@ -113,24 +89,44 @@ for (const targetBranch of branches) {
11389
// If continue fails, make a simple commit
11490
execSync(`git commit --no-edit --allow-empty-message || git commit -m "Cherry-pick ${commitSha} (with conflicts)"`, { stdio: 'inherit' });
11591
}
92+
} else if (error.message && error.message.includes('previous cherry-pick is now empty')) {
93+
// Handle empty commits (changes already exist in target branch)
94+
core.info(`Commit ${commitSha.substring(0, 7)} is empty (changes already in target branch), skipping`);
95+
execSync('git cherry-pick --skip', { stdio: 'inherit' });
11696
} else {
11797
throw error;
11898
}
11999
}
120100
}
121-
// Push the backport branch
101+
// Push the backport branch (force to handle updates)
122102
core.info(`Pushing ${backportBranch} to origin`);
123-
execSync(`git push origin ${backportBranch}`, { stdio: 'inherit' });
103+
execSync(`git push --force-with-lease origin ${backportBranch}`, { stdio: 'inherit' });
104+
105+
// Check if a PR already exists for this backport branch
106+
const { data: existingPRs } = await github.rest.pulls.list({
107+
owner: context.repo.owner,
108+
repo: context.repo.repo,
109+
head: `${context.repo.owner}:${backportBranch}`,
110+
base: targetBranch,
111+
state: 'open'
112+
});
113+
const existingPR = existingPRs.length > 0 ? existingPRs[0] : null;
114+
124115
// Create pull request
125116
const commitList = commits.map(c => `- \`${c.sha.substring(0, 7)}\` ${c.commit.message.split('\n')[0]}`).join('\n');
126117

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

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+
130126
if (hasConflicts) {
131127
prBody += `⚠️ **This PR has merge conflicts that need manual resolution.**
132128
133-
Original PR: #${prNumber}
129+
Original PR: #${prNumber} ${isMerged ? '(merged)' : '(not yet merged)'}
134130
Original Author: @${prAuthor}
135131
136132
**Cherry-picked commits (${commits.length}):**
@@ -158,7 +154,7 @@ git push --force-with-lease origin ${backportBranch}
158154
} else {
159155
prBody += `✅ Cherry-pick completed successfully with no conflicts.
160156
161-
Original PR: #${prNumber}
157+
Original PR: #${prNumber} ${isMerged ? '(merged)' : '(not yet merged)'}
162158
Original Author: @${prAuthor}
163159
164160
**Cherry-picked commits (${commits.length}):**
@@ -167,37 +163,98 @@ ${commitList}
167163
This backport was automatically created by the backport bot.`;
168164
}
169165

170-
const newPR = await github.rest.pulls.create({
171-
owner: context.repo.owner,
172-
repo: context.repo.repo,
173-
title: `[${targetBranch}] ${prTitle}`,
174-
head: backportBranch,
175-
base: targetBranch,
176-
body: prBody,
177-
draft: hasConflicts
178-
});
179-
// Add labels
180-
await github.rest.issues.addLabels({
181-
owner: context.repo.owner,
182-
repo: context.repo.repo,
183-
issue_number: newPR.data.number,
184-
labels: ['backport', hasConflicts ? 'needs-manual-resolution' : 'auto-backport']
185-
});
186-
// Link to original PR
187-
await github.rest.issues.createComment({
188-
owner: context.repo.owner,
189-
repo: context.repo.repo,
190-
issue_number: prNumber,
191-
body: `🤖 Backport PR created for \`${targetBranch}\`: #${newPR.data.number} ${hasConflicts ? '⚠️ (has conflicts)' : '✅'}`
192-
});
193-
results.push({
194-
branch: targetBranch,
195-
success: true,
196-
prNumber: newPR.data.number,
197-
prUrl: newPR.data.html_url,
198-
hasConflicts
199-
});
200-
core.info(`✅ Successfully created backport PR #${newPR.data.number}`);
166+
if (existingPR) {
167+
// Update existing PR
168+
core.info(`Found existing PR #${existingPR.number}, updating it`);
169+
await github.rest.pulls.update({
170+
owner: context.repo.owner,
171+
repo: context.repo.repo,
172+
pull_number: existingPR.number,
173+
body: prBody,
174+
draft: hasConflicts
175+
});
176+
177+
// Update labels
178+
const currentLabels = existingPR.labels.map(l => l.name);
179+
const desiredLabels = ['backport', hasConflicts ? 'needs-manual-resolution' : 'auto-backport'];
180+
181+
// Remove old labels if conflict status changed
182+
if (hasConflicts && currentLabels.includes('auto-backport')) {
183+
await github.rest.issues.removeLabel({
184+
owner: context.repo.owner,
185+
repo: context.repo.repo,
186+
issue_number: existingPR.number,
187+
name: 'auto-backport'
188+
}).catch(() => {}); // Ignore if label doesn't exist
189+
} else if (!hasConflicts && currentLabels.includes('needs-manual-resolution')) {
190+
await github.rest.issues.removeLabel({
191+
owner: context.repo.owner,
192+
repo: context.repo.repo,
193+
issue_number: existingPR.number,
194+
name: 'needs-manual-resolution'
195+
}).catch(() => {}); // Ignore if label doesn't exist
196+
}
197+
198+
// Add current labels
199+
await github.rest.issues.addLabels({
200+
owner: context.repo.owner,
201+
repo: context.repo.repo,
202+
issue_number: existingPR.number,
203+
labels: desiredLabels
204+
});
205+
206+
// Comment about the update
207+
await github.rest.issues.createComment({
208+
owner: context.repo.owner,
209+
repo: context.repo.repo,
210+
issue_number: prNumber,
211+
body: `🤖 Updated existing backport PR for \`${targetBranch}\`: #${existingPR.number} ${hasConflicts ? '⚠️ (has conflicts)' : '✅'}`
212+
});
213+
214+
results.push({
215+
branch: targetBranch,
216+
success: true,
217+
prNumber: existingPR.number,
218+
prUrl: existingPR.html_url,
219+
hasConflicts,
220+
updated: true
221+
});
222+
core.info(`✅ Successfully updated backport PR #${existingPR.number}`);
223+
} else {
224+
// Create new PR
225+
const newPR = await github.rest.pulls.create({
226+
owner: context.repo.owner,
227+
repo: context.repo.repo,
228+
title: `[${targetBranch}] ${prTitle}`,
229+
head: backportBranch,
230+
base: targetBranch,
231+
body: prBody,
232+
draft: hasConflicts
233+
});
234+
// Add labels
235+
await github.rest.issues.addLabels({
236+
owner: context.repo.owner,
237+
repo: context.repo.repo,
238+
issue_number: newPR.data.number,
239+
labels: ['backport', hasConflicts ? 'needs-manual-resolution' : 'auto-backport']
240+
});
241+
// Link to original PR
242+
await github.rest.issues.createComment({
243+
owner: context.repo.owner,
244+
repo: context.repo.repo,
245+
issue_number: prNumber,
246+
body: `🤖 Backport PR created for \`${targetBranch}\`: #${newPR.data.number} ${hasConflicts ? '⚠️ (has conflicts)' : '✅'}`
247+
});
248+
results.push({
249+
branch: targetBranch,
250+
success: true,
251+
prNumber: newPR.data.number,
252+
prUrl: newPR.data.html_url,
253+
hasConflicts,
254+
updated: false
255+
});
256+
core.info(`✅ Successfully created backport PR #${newPR.data.number}`);
257+
}
201258
} catch (error) {
202259
core.error(`❌ Failed to backport to ${targetBranch}: ${error.message}`);
203260
// Comment on original PR about the failure
@@ -229,7 +286,8 @@ core.info('Backport Summary');
229286
core.info('========================================');
230287
for (const result of results) {
231288
if (result.success) {
232-
core.info(`✅ ${result.branch}: PR #${result.prNumber} ${result.hasConflicts ? '(has conflicts)' : ''}`);
289+
const action = result.updated ? 'Updated' : 'Created';
290+
core.info(`✅ ${result.branch}: ${action} PR #${result.prNumber} ${result.hasConflicts ? '(has conflicts)' : ''}`);
233291
} else {
234292
core.error(`❌ ${result.branch}: ${result.error}`);
235293
}

.github/scripts/extract-branches.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,20 @@ module.exports = async ({ github, context, core }) => {
2121
const prNumber = context.payload.pull_request?.number || context.payload.issue?.number;
2222

2323
if (!prNumber) {
24-
core.setFailed('Could not determine PR number from event');
24+
core.warning('Could not determine PR number from event - skipping backport');
2525
return [];
2626
}
2727

2828
// Check PR body
2929
if (context.payload.pull_request?.body) {
3030
const prBody = context.payload.pull_request.body;
3131
// Strict ASCII, anchored; allow X.Y or X.Y.Z
32-
const bodyMatches = prBody.matchAll(/^\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/gmi);
33-
branches.push(...Array.from(bodyMatches, m => m[1]));
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]));
37+
}
3438
}
3539

3640
// Check all comments
@@ -41,15 +45,18 @@ module.exports = async ({ github, context, core }) => {
4145
});
4246

4347
for (const comment of comments.data) {
44-
const commentMatches = comment.body.matchAll(/^\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/gmi);
45-
branches.push(...Array.from(commentMatches, m => m[1]));
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+
}
4653
}
4754

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

5158
if (branches.length === 0) {
52-
core.setFailed('No valid release branches found in /cherry-pick comments');
59+
core.info('No cherry-pick requests found - skipping backport');
5360
return [];
5461
}
5562

.github/workflows/cherrypick.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
name: Cherry-Pick
1616

1717
on:
18-
pull_request_target:
19-
types: [closed]
2018
issue_comment:
2119
types: [created]
2220

@@ -29,10 +27,9 @@ jobs:
2927
backport:
3028
name: Backport PR
3129
runs-on: ubuntu-latest
32-
# Run on merged PRs OR on /cherry-pick comments
30+
# Run on /cherry-pick comments on PRs
3331
if: |
34-
(github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) ||
35-
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/cherry-pick'))
32+
github.event.issue.pull_request && startsWith(github.event.comment.body, '/cherry-pick')
3633
3734
steps:
3835
- name: Checkout repository
@@ -50,12 +47,14 @@ jobs:
5047
return await run({ github, context, core });
5148
5249
- name: Configure git
50+
if: steps.extract-branches.outputs.result != '[]'
5351
run: |
5452
git config user.name "nvidia-backport-bot"
5553
git config user.email "[email protected]"
5654
5755
- name: Backport to release branches
5856
id: backport
57+
if: steps.extract-branches.outputs.result != '[]'
5958
uses: actions/github-script@v8
6059
env:
6160
BRANCHES_JSON: ${{ steps.extract-branches.outputs.result }}

0 commit comments

Comments
 (0)