diff --git a/.github/actions/combine-dependabot-prs/action.yaml b/.github/actions/combine-dependabot-prs/action.yaml new file mode 100644 index 0000000..6029ec9 --- /dev/null +++ b/.github/actions/combine-dependabot-prs/action.yaml @@ -0,0 +1,45 @@ +name: "Combine Dependabot PRs" +description: "Combine open dependency PRs into a single branch and PR" +inputs: + labels: + description: "Label used to select PRs" + required: false + default: "dependencies" + pr_title: + description: "Combined pull request title" + required: false + default: "CCM-9336: Combined Dependabot PRs" + combine_branch_name: + description: "Branch used for the combined pull request" + required: false + default: "dependabotCombined" + pr_body_header: + description: "Header text used at the top of the combined pull request body" + required: false + default: "CCM-9336: Combined Dependabot PRs" +runs: + using: "composite" + steps: + - name: "Combine pull requests" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_LABEL: ${{ inputs.labels }} + INPUT_PR_TITLE: ${{ inputs.pr_title }} + INPUT_COMBINE_BRANCH: ${{ inputs.combine_branch_name }} + INPUT_PR_BODY_HEADER: ${{ inputs.pr_body_header }} + with: + script: | + const path = require("node:path"); + const run = require(path.join(process.env.GITHUB_ACTION_PATH, "combine-dependabot-prs.js")); + + await run({ + github, + context, + core, + inputs: { + label: process.env.INPUT_LABEL, + prTitle: process.env.INPUT_PR_TITLE, + combineBranch: process.env.INPUT_COMBINE_BRANCH, + prBodyHeader: process.env.INPUT_PR_BODY_HEADER, + }, + }); diff --git a/.github/actions/combine-dependabot-prs/combine-dependabot-prs.js b/.github/actions/combine-dependabot-prs/combine-dependabot-prs.js new file mode 100644 index 0000000..60bb073 --- /dev/null +++ b/.github/actions/combine-dependabot-prs/combine-dependabot-prs.js @@ -0,0 +1,111 @@ +module.exports = async ({ github, context, core, inputs }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + const baseBranch = "main"; + const combineBranch = inputs.combineBranch; + const prTitle = inputs.prTitle; + const prBodyHeader = inputs.prBodyHeader; + const requiredLabel = (inputs.label || "dependencies").trim(); + + const allOpenPrs = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: "open", + per_page: 100, + }); + + const dependencyPrs = allOpenPrs + .filter((pr) => pr.labels.some((label) => label.name === requiredLabel)) + .sort((a, b) => a.number - b.number); + + if (dependencyPrs.length === 0) { + core.info("No open dependency PRs found; nothing to combine."); + return; + } + + const { data: baseRef } = await github.rest.git.getRef({ + owner, + repo, + ref: `heads/${baseBranch}`, + }); + + const baseSha = baseRef.object.sha; + + try { + await github.rest.git.getRef({ + owner, + repo, + ref: `heads/${combineBranch}`, + }); + + await github.rest.git.updateRef({ + owner, + repo, + ref: `heads/${combineBranch}`, + sha: baseSha, + force: true, + }); + } catch (error) { + if (error.status !== 404) { + throw error; + } + + await github.rest.git.createRef({ + owner, + repo, + ref: `refs/heads/${combineBranch}`, + sha: baseSha, + }); + } + + for (const pr of dependencyPrs) { + try { + await github.rest.repos.merge({ + owner, + repo, + base: combineBranch, + head: pr.head.sha, + commit_message: `Merge #${pr.number} into ${combineBranch}`, + }); + core.info(`Merged PR #${pr.number} (${pr.head.ref}) into ${combineBranch}`); + } catch (error) { + core.setFailed( + `Failed to merge PR #${pr.number} (${pr.head.ref}) into ${combineBranch}: ${error.message}`, + ); + throw error; + } + } + + const includedPrLines = dependencyPrs.map((pr) => `- #${pr.number}`).join("\n"); + const prBody = `${prBodyHeader}\n\nIncluded PRs:\n${includedPrLines}`; + + const existingCombinedPr = allOpenPrs.find( + (pr) => + pr.base.ref === baseBranch && + pr.head.ref === combineBranch && + pr.user?.login !== "dependabot[bot]", + ); + + if (existingCombinedPr) { + await github.rest.pulls.update({ + owner, + repo, + pull_number: existingCombinedPr.number, + title: prTitle, + body: prBody, + }); + core.info(`Updated existing combined PR #${existingCombinedPr.number}`); + return; + } + + const { data: newPr } = await github.rest.pulls.create({ + owner, + repo, + title: prTitle, + head: combineBranch, + base: baseBranch, + body: prBody, + }); + + core.info(`Created combined PR #${newPr.number}`); +};