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)' 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' # A workflow run is made up of one or more jobs that can run sequentially or in parallel 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 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 statusResponse = await github.rest.repos.getCombinedStatusForRef({ owner: context.repo.owner, repo: context.repo.repo, ref: branch }); const state = statusResponse['data']['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 });