Merge PRs #255
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Merge PRs | |
on: | |
workflow_dispatch: | |
inputs: | |
branch: | |
description: "Branch from which to merge PRs" | |
required: false | |
dry-run: | |
description: "Whether to run in dry run mode" | |
required: false | |
default: 'false' | |
schedule: | |
- cron: '10 7 * * *' # https://crontab.guru/#10_7_*_*_* | |
jobs: | |
merge-prs: | |
name: Merge PRs | |
runs-on: ubuntu-latest | |
steps: | |
- name: Merge PRs | |
env: | |
QUERY: is:pr author:web3-bot state:open archived:false | |
BRANCH: ${{ github.event.inputs.branch }} | |
DRY_RUN: ${{ github.event.inputs.dry-run }} | |
uses: actions/github-script@v7 | |
with: | |
github-token: ${{ secrets.UCI_GITHUB_TOKEN }} | |
retries: 0 | |
script: | | |
const request = async function(req, opts) { | |
try { | |
return await req(opts) | |
} catch(err) { | |
opts._attempt = (opts._attempt || 0) + 1 | |
if (err.status === 403) { | |
if (err.response.headers['x-ratelimit-remaining'] === '0') { | |
const retryAfter = err.response.headers['x-ratelimit-reset'] - Math.floor(Date.now() / 1000) || 1 | |
core.info(`Rate limit exceeded, retrying in ${retryAfter} seconds`) | |
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)) | |
return request(req, opts) | |
} | |
if (err.message.toLowerCase().includes('secondary rate limit')) { | |
const retryAfter = Math.pow(2, opts._attempt) | |
core.info(`Secondary rate limit exceeded, retrying in ${retryAfter} seconds`) | |
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)) | |
return request(req, opts) | |
} | |
} | |
throw err | |
} | |
} | |
github.hook.wrap('request', request) | |
let q = process.env.QUERY | |
if (process.env.BRANCH) { | |
q += ` head:${process.env.BRANCH}` | |
} | |
core.info(`Looking for PRs matching the query: ${q}`) | |
const items = await github.paginate(github.rest.search.issuesAndPullRequests, { | |
q | |
}) | |
core.info(`Filtering out the PRs that cannot be merged`) | |
const prs = [] | |
for (const item of items) { | |
core.info(`Retrieving ${item.html_url}`) | |
const [_, owner, repo] = item.url.match(/repos\/(.+?)\/(.+?)\/issues/) | |
const {data: pr} = await github.rest.pulls.get({ | |
owner, | |
repo, | |
pull_number: item.number | |
}) | |
if (pr.mergeable_state == 'clean') { | |
const commits = await github.paginate(github.rest.pulls.listCommits, { | |
owner, | |
repo, | |
pull_number: item.number | |
}) | |
if (commits.filter(c => c.commit.author.name == 'web3-bot').length != commits.length) { | |
core.info(`${pr.html_url} cannot be merged because it contains commits from other authors`) | |
} else { | |
core.info(`${pr.html_url} can be merged`) | |
prs.push(pr) | |
} | |
} else { | |
core.info(`${pr.html_url} cannot be merged because it is in ${pr.mergeable_state} state`) | |
} | |
} | |
core.info(`Attempting to merge the PRs`) | |
const failed = [] | |
for (const pr of prs) { | |
core.info(`Merging ${pr.html_url}`) | |
if (process.env.DRY_RUN == 'true') { | |
core.info(`Skipping PR merge because this is a dry run`) | |
continue | |
} | |
try { | |
let mergeMethods = ['squash', 'merge', 'rebase'] | |
let merge = undefined | |
while (merge == undefined && mergeMethods.length != 0) { | |
const mergeMethod = mergeMethods.shift() | |
try { | |
merge = await github.rest.pulls.merge({ | |
owner: pr.base.repo.owner.login, | |
repo: pr.base.repo.name, | |
pull_number: pr.number, | |
merge_method: mergeMethod | |
}) | |
} catch(error) { | |
const message = error.message.toLowerCase() | |
if (!(message.includes(mergeMethod) && message.includes('not allowed on this repository'))) { | |
throw error | |
} | |
} | |
} | |
if (!merge) { | |
throw new Error('No merge methods are allowed on this repository.') | |
} | |
core.info(`${pr.html_url} merged successfully`) | |
} catch(error) { | |
core.error(`Couldn't merge ${pr.html_url}, got: ${error}`) | |
failed.push(pr) | |
} | |
} | |
if (failed.length != 0) { | |
throw new Error(`Failed to merge ${failed.length} PRs`) | |
} |