From 7300e97fd2f0b0153aad619ed29d459030938bb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:46:50 -0500 Subject: [PATCH] fix: add manual backport workflow (#128) (#131) (cherry picked from commit c57fe6af0289d48f88984127897ad84327beff61) Signed-off-by: matttrach Co-authored-by: Matt Trachier --- .github/workflows/backport-pr-manual.yml | 123 +++++++++++++++++++++++ .github/workflows/backport.yml | 2 +- 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/backport-pr-manual.yml diff --git a/.github/workflows/backport-pr-manual.yml b/.github/workflows/backport-pr-manual.yml new file mode 100644 index 0000000..636b2b6 --- /dev/null +++ b/.github/workflows/backport-pr-manual.yml @@ -0,0 +1,123 @@ +name: 'Auto Cherry-Pick to Release Branches' + +on: + workflow_dispatch: + inputs: + merge_commit_sha: + description: 'The sha of the merge commit from the main PR.' + required: true + +jobs: + create-cherry-pick-prs: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + actions: write + steps: + - name: 'Wait for merge to settle' + run: sleep 10 + - name: 'Checkout Repository' + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 https://github.com/actions/checkout + with: + fetch-depth: 0 + - name: 'Find Issues and Create Cherry-Pick PRs' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 https://github.com/actions/github-script + env: + MERGE_COMMIT_SHA: ${{ inputs.merge_commit_sha }} + with: + script: | + const execSync = require('child_process').execSync; + const owner = github.repository_owner; + const repo = github.repository; + const mergeCommitSha = process.env.MERGE_COMMIT_SHA; + const assignees = ['matttrach', 'jiaqiluo', 'HarrisonWAffel']; + + const { data: associatedPrs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner, + repo, + commit_sha: mergeCommitSha + }); + const pr = associatedPrs.find(p => p.base.ref === 'main' && p.merged_at); + if (!pr) { + core.info(`No merged PR found for commit ${mergeCommitSha}. This may have been a direct push. Exiting.`); + return; + } + core.info(`Found associated PR: #${pr.number}`); + + // https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests + core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`); + const { data: searchResults } = await github.request('GET /search/issues', { + q: `is:issue state:open label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}`, + advanced_search: true, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + if (searchResults.total_count === 0) { + core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`); + return; + } + const mainIssue = searchResults.items[0]; + core.info(`Found main issue: #${mainIssue.number}`); + + // https://docs.github.com/en/rest/issues/sub-issues?apiVersion=2022-11-28#add-sub-issue + core.info(`Fetching sub-issues for main issue #${mainIssue.number}`); + const { data: subIssues } = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', { + owner: owner, + repo: repo, + issue_number: mainIssue.number, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + if (subIssues.length === 0) { + core.info(`No sub-issues found for issue #${mainIssue.number}. Exiting.`); + return; + } + core.info(`Found ${subIssues.length} sub-issues.`); + + for (const subIssue of subIssues) { + const subIssueNumber = subIssue.number; + // Find the release label directly on the sub-issue object + const releaseLabel = subIssue.labels.find(label => label.name.startsWith('release/v')); + if (!releaseLabel) { + core.warning(`Sub-issue #${subIssueNumber} has no 'release/v...' label. Skipping.`); + continue; + } + const targetBranch = releaseLabel.name + core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`); + const newBranchName = `backport-${pr.number}-${targetBranch.replace(/\//g, '-')}`; + execSync(`git config user.name "github-actions[bot]"`); + execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`); + execSync(`git fetch origin ${targetBranch}`); + execSync(`git checkout -b ${newBranchName} origin/${targetBranch}`); + execSync(`git cherry-pick -x ${mergeCommitSha} -X theirs`); + execSync(`git push origin ${newBranchName}`); + + core.info(`Creating pull request for branch ${newBranchName} targeting ${targetBranch}...`); + const { data: newPr } = await github.rest.pulls.create({ + owner, + repo, + title: pr.title, + head: newBranchName, + base: targetBranch, + body: [ + `This pull request cherry-picks the changes from #${pr.number} into ${targetBranch}`, + `Addresses #${subIssueNumber} for #${mainIssue.number}`, + `**WARNING!**: to avoid having to resolve merge conflicts this PR is generated with 'git cherry-pick -X theirs'.`, + `Please make sure to carefully inspect this PR so that you don't accidentally revert anything!`, + `Please add the proper milestone to this PR`, + `Copied from main PR:`, + `${pr.body}` + ].join("\n\n") + }); + const prNumber = newPr.number + await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: prNumber, + assignees: assignees + }); + } diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 9d6c677..b94f46a 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -61,7 +61,7 @@ jobs: `Backport #${prNumber} to ${labelName} for #${parentIssueNumber}`, `Copied from PR:`, `${pr.body}` - ].join("\n\n") + ].join("\n\n"), labels: [labelName], assignees: assignees });