Skip to content

Merge PRs

Merge PRs #260

Workflow file for this run

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`)
}