Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .github/workflows/merge-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: 'Combine PRs'

# Controls when the action will run - in this case triggered manually
on:
workflow_dispatch:
inputs:
branchPrefix:
description: 'Branch prefix to find combinable PRs based on'
required: true
default: 'dependabot'
mustBeGreen:
description: 'Only combine PRs that are green (status is success). Set to false if repo does not run checks'
type: boolean
required: true
default: true
combineBranchName:
description: 'Name of the branch to combine PRs into'
required: true
default: 'combine-prs-branch'
ignoreLabel:
description: 'Exclude PRs with this label'
required: true
default: 'nocombine'

jobs:
# This workflow contains a single job called "combine-prs"
combine-prs:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/github-script@v6
Comment on lines +25 to +33
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow creates branches and opens PRs but doesn’t declare any permissions:. With the default restricted GITHUB_TOKEN permissions (or org-level hardening), git.createRef, repos.merge, and pulls.create can fail. Add explicit least-privilege permissions (e.g., contents: write and pull-requests: write) at the workflow or job level so the action is reliable across repo settings.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repo’s other workflows use actions/github-script@v7 (e.g. .github/workflows/build.yml:99). Consider bumping from @v6 to @v7 to stay consistent and pick up fixes/security updates.

Suggested change
- uses: actions/github-script@v6
- uses: actions/github-script@v7

Copilot uses AI. Check for mistakes.
id: create-combined-pr
name: Create Combined PR
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
owner: context.repo.owner,
repo: context.repo.repo
});
let branchesAndPRStrings = [];
let baseBranch = null;
let baseBranchSHA = null;
for (const pull of pulls) {
const branch = pull['head']['ref'];
console.log('Pull for branch: ' + branch);
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
console.log('Branch matched prefix: ' + branch);
let statusOK = true;
if(${{ github.event.inputs.mustBeGreen }}) {
console.log('Checking green status: ' + branch);
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number:$pull_number) {
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
state
}
}
}
}
}
}
}`
const vars = {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull['number']
};
const result = await github.graphql(stateQuery, vars);
const [{ commit }] = result.repository.pullRequest.commits.nodes;
const state = commit.statusCheckRollup.state
console.log('Validating status: ' + state);
if(state != 'SUCCESS') {
console.log('Discarding ' + branch + ' with status ' + state);
statusOK = false;
Comment on lines +75 to +80
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commit.statusCheckRollup can be null (e.g., no checks configured, checks disabled, or insufficient permissions), and commits.nodes can be empty. Accessing .state without guarding will throw and fail the workflow. Add a defensive check and treat missing/unknown rollup state as non-green (or skip with a clear message).

Suggested change
const [{ commit }] = result.repository.pullRequest.commits.nodes;
const state = commit.statusCheckRollup.state
console.log('Validating status: ' + state);
if(state != 'SUCCESS') {
console.log('Discarding ' + branch + ' with status ' + state);
statusOK = false;
const commitNodes = result.repository.pullRequest.commits.nodes || [];
const latestCommitNode = commitNodes.length > 0 ? commitNodes[0] : null;
const commit = latestCommitNode ? latestCommitNode.commit : null;
const state = commit && commit.statusCheckRollup ? commit.statusCheckRollup.state : null;
if (!state) {
console.log('Discarding ' + branch + ' because no status check rollup state is available');
statusOK = false;
} else {
console.log('Validating status: ' + state);
if(state != 'SUCCESS') {
console.log('Discarding ' + branch + ' with status ' + state);
statusOK = false;
}

Copilot uses AI. Check for mistakes.
}
}
console.log('Checking labels: ' + branch);
const labels = pull['labels'];
for(const label of labels) {
const labelName = label['name'];
console.log('Checking label: ' + labelName);
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
console.log('Discarding ' + branch + ' with label ' + labelName);
statusOK = false;
}
}
if (statusOK) {
console.log('Adding branch to array: ' + branch);
const prString = '#' + pull['number'] + ' ' + pull['title'];
branchesAndPRStrings.push({ branch, prString });
baseBranch = pull['base']['ref'];
baseBranchSHA = pull['base']['sha'];
}
Comment on lines +93 to +99
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseBranch / baseBranchSHA are set from whichever matching PR is processed last. If matching PRs target different base branches, the combined branch/PR base becomes nondeterministic and merges can be incorrect. Validate that all selected PRs share the same pull.base.ref (and fail/skip otherwise) before proceeding.

Copilot uses AI. Check for mistakes.
}
}
if (branchesAndPRStrings.length == 0) {
core.setFailed('No PRs/branches matched criteria');
return;
}
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
sha: baseBranchSHA
});
Comment on lines +106 to +112
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseBranchSHA is taken from pull.base.sha (the base SHA recorded on the PR), which may be stale compared to the current tip of the base branch. Creating the combined branch from an old SHA can increase conflicts and produce an out-of-date combined PR. Prefer resolving the current base branch head SHA via git.getRef (or equivalent) once the base branch is determined.

Copilot uses AI. Check for mistakes.
} catch (error) {
console.log(error);
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
return;
}
Comment on lines +106 to +117
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says the workflow can merge into a new or existing combined branch, but the implementation fails the run if the branch already exists. Consider supporting an existing branch (e.g., reset it to the chosen base SHA, or allow reusing it) instead of hard-failing on createRef errors.

Copilot uses AI. Check for mistakes.

let combinedPRs = [];
let mergeFailedPRs = [];
for(const { branch, prString } of branchesAndPRStrings) {
try {
await github.rest.repos.merge({
owner: context.repo.owner,
repo: context.repo.repo,
base: '${{ github.event.inputs.combineBranchName }}',
head: branch,
});
Comment on lines +123 to +128
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repos.merge uses head: branch where branch is pull.head.ref. This fails for PRs from forks because the API expects head as owner:ref in that case. Use pull.head.label or construct ${pull.head.repo.owner.login}:${pull.head.ref} when pull.head.repo.full_name !== context.repo.owner + '/' + context.repo.repo.

Copilot uses AI. Check for mistakes.
console.log('Merged branch ' + branch);
combinedPRs.push(prString);
} catch (error) {
console.log('Failed to merge branch ' + branch);
mergeFailedPRs.push(prString);
}
}

console.log('Creating combined PR');
const combinedPRsString = combinedPRs.join('\n');
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
if(mergeFailedPRs.length > 0) {
const mergeFailedPRsString = mergeFailedPRs.join('\n');
body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
}
await github.rest.pulls.create({
Comment on lines +119 to +144
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow always creates a combined PR even if every merge failed (so combinedPRs is empty). That produces a noisy PR with no combined content. Consider failing early (or skipping PR creation) when combinedPRs.length === 0, and include the merge error messages in the output/PR body to aid debugging.

Copilot uses AI. Check for mistakes.
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Combined PR',
head: '${{ github.event.inputs.combineBranchName }}',
base: baseBranch,
body: body
});
Comment on lines +29 to +151

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}

Copilot Autofix

AI 25 days ago

In general, the fix is to add an explicit permissions block to the workflow (either at the root or under the specific job) that grants only the scopes required for the actions performed. This documents the workflow’s needs and prevents it from silently inheriting overly broad defaults.

For this specific workflow in .github/workflows/merge-prs.yml, the script reads and writes pull requests and branches. The write operations include creating a branch (git.createRef), merging branches (repos.merge), and creating a pull request (pulls.create). These operations require at least:

  • contents: write (for creating refs/branches and merging)
  • pull-requests: write (for creating pull requests)

To fix the issue without changing existing behavior, add a permissions section at the job level for combine-prs, right under runs-on: ubuntu-latest. This keeps the permissions scoped only to that job, rather than globally for all jobs in the workflow. The block should look like:

    permissions:
      contents: write
      pull-requests: write

No additional imports, methods, or other structural changes are necessary, because this is entirely a YAML configuration change.

Suggested changeset 1
.github/workflows/merge-prs.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/merge-prs.yml b/.github/workflows/merge-prs.yml
--- a/.github/workflows/merge-prs.yml
+++ b/.github/workflows/merge-prs.yml
@@ -27,6 +27,9 @@
   combine-prs:
     # The type of runner that the job will run on
     runs-on: ubuntu-latest
+    permissions:
+      contents: write
+      pull-requests: write
 
     # Steps represent a sequence of tasks that will be executed as part of the job
     steps:
EOF
@@ -27,6 +27,9 @@
combine-prs:
# The type of runner that the job will run on
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +144 to +151
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating the PR unconditionally with a fixed title/head/base will fail if an open PR already exists for the same headbase (GitHub typically allows only one open PR per head/base). Consider checking for an existing PR for combineBranchName and updating it (or closing/replacing) instead of always calling pulls.create.

Copilot uses AI. Check for mistakes.
Loading