Conversation
Bundle Size Report 📦
Bundle Analysis ReportsThe detailed bundle analysis reports are available in the workflow artifacts:
Download the artifacts from the workflow run to view interactive visualizations. Bundle size tracking is now active! This helps prevent bundle bloat. |
| 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 | ||
| 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; | ||
| } | ||
| } | ||
| 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']; | ||
| } | ||
| } | ||
| } | ||
| 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 | ||
| }); | ||
| } catch (error) { | ||
| console.log(error); | ||
| core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?'); | ||
| return; | ||
| } | ||
|
|
||
| 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, | ||
| }); | ||
| 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({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| title: 'Combined PR', | ||
| head: '${{ github.event.inputs.combineBranchName }}', | ||
| base: baseBranch, | ||
| body: body | ||
| }); No newline at end of file |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 3 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: writeNo additional imports, methods, or other structural changes are necessary, because this is entirely a YAML configuration change.
| @@ -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: |
There was a problem hiding this comment.
Pull request overview
Adds a manually triggered GitHub Actions workflow to consolidate multiple qualifying pull requests into a single “combined” PR, intended to reduce maintainer overhead (e.g., batching Dependabot PRs).
Changes:
- Introduces
.github/workflows/merge-prs.ymlwithworkflow_dispatchinputs to filter PRs by branch prefix, required “green” status, and an ignore label. - Automatically creates a combined branch, attempts to merge each qualifying PR branch into it, then opens a new PR summarizing included/skipped PRs.
| 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 |
There was a problem hiding this comment.
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.
| 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']; | ||
| } |
There was a problem hiding this comment.
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.
| try { | ||
| await github.rest.git.createRef({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}', | ||
| sha: baseBranchSHA | ||
| }); |
There was a problem hiding this comment.
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.
| try { | ||
| await github.rest.git.createRef({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}', | ||
| sha: baseBranchSHA | ||
| }); | ||
| } catch (error) { | ||
| console.log(error); | ||
| core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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).
| 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; | |
| } |
| await github.rest.repos.merge({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| base: '${{ github.event.inputs.combineBranchName }}', | ||
| head: branch, | ||
| }); |
There was a problem hiding this comment.
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.
| 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, | ||
| }); | ||
| 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({ |
There was a problem hiding this comment.
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.
| await github.rest.pulls.create({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| title: 'Combined PR', | ||
| head: '${{ github.event.inputs.combineBranchName }}', | ||
| base: baseBranch, | ||
| body: body | ||
| }); No newline at end of file |
There was a problem hiding this comment.
Creating the PR unconditionally with a fixed title/head/base will fail if an open PR already exists for the same head → base (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.
|
|
||
| # Steps represent a sequence of tasks that will be executed as part of the job | ||
| steps: | ||
| - uses: actions/github-script@v6 |
There was a problem hiding this comment.
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.
| - uses: actions/github-script@v6 | |
| - uses: actions/github-script@v7 |
This pull request adds a new GitHub Actions workflow to automate the process of combining multiple pull requests that match certain criteria into a single PR. The workflow can be run manually and provides options to filter which PRs are combined, check their status, and handle merge conflicts.
Key additions in this workflow:
Automated PR Combination Workflow:
.github/workflows/merge-prs.ymlthat allows maintainers to combine multiple PRs based on branch prefix, status checks, and labels, with customizable input parameters.