| 1 | name: 'Combine PRs' |
| 2 | |
| 3 | # Controls when the action will run - in this case triggered manually |
| 4 | on: |
| 5 | workflow_dispatch: |
| 6 | inputs: |
| 7 | branchPrefix: |
| 8 | description: 'Branch prefix to find combinable PRs based on' |
| 9 | required: true |
| 10 | default: 'dependabot' |
| 11 | mustBeGreen: |
| 12 | description: 'Only combine PRs that are green (status is success)' |
| 13 | required: true |
| 14 | default: true |
| 15 | combineBranchName: |
| 16 | description: 'Name of the branch to combine PRs into' |
| 17 | required: true |
| 18 | default: 'combine-prs-branch' |
| 19 | ignoreLabel: |
| 20 | description: 'Exclude PRs with this label' |
| 21 | required: true |
| 22 | default: 'nocombine' |
| 23 | |
| 24 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel |
| 25 | jobs: |
| 26 | # This workflow contains a single job called "combine-prs" |
| 27 | combine-prs: |
| 28 | # The type of runner that the job will run on |
| 29 | runs-on: ubuntu-latest |
| 30 | |
| 31 | # Steps represent a sequence of tasks that will be executed as part of the job |
| 32 | steps: |
| 33 | - uses: actions/github-script@v6 |
| 34 | id: create-combined-pr |
| 35 | name: Create Combined PR |
| 36 | with: |
| 37 | github-token: ${{secrets.GITHUB_TOKEN}} |
| 38 | script: | |
| 39 | const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', { |
| 40 | owner: context.repo.owner, |
| 41 | repo: context.repo.repo |
| 42 | }); |
| 43 | let branchesAndPRStrings = []; |
| 44 | let baseBranch = null; |
| 45 | let baseBranchSHA = null; |
| 46 | for (const pull of pulls) { |
| 47 | const branch = pull['head']['ref']; |
| 48 | console.log('Pull for branch: ' + branch); |
| 49 | if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) { |
| 50 | console.log('Branch matched prefix: ' + branch); |
| 51 | let statusOK = true; |
| 52 | if(${{ github.event.inputs.mustBeGreen }}) { |
| 53 | console.log('Checking green status: ' + branch); |
| 54 | const statusResponse = await github.rest.repos.getCombinedStatusForRef({ |
| 55 | owner: context.repo.owner, |
| 56 | repo: context.repo.repo, |
| 57 | ref: branch |
| 58 | }); |
| 59 | const state = statusResponse['data']['state']; |
| 60 | console.log('Validating status: ' + state); |
| 61 | if(state != 'success') { |
| 62 | console.log('Discarding ' + branch + ' with status ' + state); |
| 63 | statusOK = false; |
| 64 | } |
| 65 | } |
| 66 | console.log('Checking labels: ' + branch); |
| 67 | const labels = pull['labels']; |
| 68 | for(const label of labels) { |
| 69 | const labelName = label['name']; |
| 70 | console.log('Checking label: ' + labelName); |
| 71 | if(labelName == '${{ github.event.inputs.ignoreLabel }}') { |
| 72 | console.log('Discarding ' + branch + ' with label ' + labelName); |
| 73 | statusOK = false; |
| 74 | } |
| 75 | } |
| 76 | if (statusOK) { |
| 77 | console.log('Adding branch to array: ' + branch); |
| 78 | const prString = '#' + pull['number'] + ' ' + pull['title']; |
| 79 | branchesAndPRStrings.push({ branch, prString }); |
| 80 | baseBranch = pull['base']['ref']; |
| 81 | baseBranchSHA = pull['base']['sha']; |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | if (branchesAndPRStrings.length == 0) { |
| 86 | core.setFailed('No PRs/branches matched criteria'); |
| 87 | return; |
| 88 | } |
| 89 | try { |
| 90 | await github.rest.git.createRef({ |
| 91 | owner: context.repo.owner, |
| 92 | repo: context.repo.repo, |
| 93 | ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}', |
| 94 | sha: baseBranchSHA |
| 95 | }); |
| 96 | } catch (error) { |
| 97 | console.log(error); |
| 98 | core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?'); |
| 99 | return; |
| 100 | } |
| 101 | |
| 102 | let combinedPRs = []; |
| 103 | let mergeFailedPRs = []; |
| 104 | for(const { branch, prString } of branchesAndPRStrings) { |
| 105 | try { |
| 106 | await github.rest.repos.merge({ |
| 107 | owner: context.repo.owner, |
| 108 | repo: context.repo.repo, |
| 109 | base: '${{ github.event.inputs.combineBranchName }}', |
| 110 | head: branch, |
| 111 | }); |
| 112 | console.log('Merged branch ' + branch); |
| 113 | combinedPRs.push(prString); |
| 114 | } catch (error) { |
| 115 | console.log('Failed to merge branch ' + branch); |
| 116 | mergeFailedPRs.push(prString); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | console.log('Creating combined PR'); |
| 121 | const combinedPRsString = combinedPRs.join('\n'); |
| 122 | let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString; |
| 123 | if(mergeFailedPRs.length > 0) { |
| 124 | const mergeFailedPRsString = mergeFailedPRs.join('\n'); |
| 125 | body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString |
| 126 | } |
| 127 | await github.rest.pulls.create({ |
| 128 | owner: context.repo.owner, |
| 129 | repo: context.repo.repo, |
| 130 | title: 'Combined PR', |
| 131 | head: '${{ github.event.inputs.combineBranchName }}', |
| 132 | base: baseBranch, |
| 133 | body: body |
| 134 | }); |