diff --git a/.github/scripts/add-labels-from-comment.js b/.github/scripts/add-labels-from-comment.js new file mode 100644 index 000000000..0865b5a24 --- /dev/null +++ b/.github/scripts/add-labels-from-comment.js @@ -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.` + }; + } +}; + diff --git a/.github/scripts/backport.js b/.github/scripts/backport.js index 85522d2c6..06b59f09c 100644 --- a/.github/scripts/backport.js +++ b/.github/scripts/backport.js @@ -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({ @@ -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}):** @@ -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}):** diff --git a/.github/scripts/extract-branches.js b/.github/scripts/extract-branches.js index f146e2c67..44bd2bcde 100644 --- a/.github/scripts/extract-branches.js +++ b/.github/scripts/extract-branches.js @@ -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 []; } diff --git a/.github/workflows/cherrypick.yml b/.github/workflows/cherrypick.yml index c8a2cbb5c..cf49ef9f8 100644 --- a/.github/workflows/cherrypick.yml +++ b/.github/workflows/cherrypick.yml @@ -17,6 +17,8 @@ name: Cherry-Pick on: issue_comment: types: [created] + pull_request: + types: [closed] permissions: contents: write @@ -24,12 +26,42 @@ permissions: 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 "noreply@nvidia.com" + + - 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 @@ -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: