Create PRs #372
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: Create 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 1 * * *' # https://crontab.guru/#10_1_*_*_* | |
jobs: | |
create-prs: | |
name: Create PRs | |
runs-on: ubuntu-latest | |
steps: | |
- name: Create PRs | |
env: | |
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) | |
core.info(`Looking for repositories the user has direct access to`) | |
const items = await github.paginate(github.rest.repos.listForAuthenticatedUser, { | |
affiliation: 'collaborator' | |
}) | |
core.info(`Filtering out the repositories without unmerged branches`) | |
const repoBranches = [] | |
for (const item of items) { | |
if (item.archived) { | |
core.info(`Skipping archived repository ${item.html_url}`) | |
continue | |
} | |
core.info(`Looking for matching branches for ${item.html_url}`) | |
const branches = (await github.paginate(github.rest.repos.listBranches, { | |
owner: item.owner.login, | |
repo: item.name | |
})).filter(branch => { | |
if (process.env.BRANCH) { | |
return branch.name == process.env.BRANCH | |
} else { | |
return branch.name.startsWith('uci/') | |
} | |
}) | |
for (const branch of branches) { | |
core.info(`Checking if ${item.html_url}/tree/${branch.name} requires a PR to be created`) | |
const {data: pulls} = await github.rest.repos.listPullRequestsAssociatedWithCommit({ | |
owner: item.owner.login, | |
repo: item.name, | |
commit_sha: branch.commit.sha | |
}) | |
if (pulls.length == 0) { | |
core.info(`PR for ${item.html_url}/tree/${branch.name} should be created`) | |
repoBranches.push({repo: item, branch}) | |
} else { | |
core.info(`PR for ${item.html_url}/tree/${branch.name} already exists`) | |
} | |
} | |
} | |
core.info(`Attempting to create the PRs`) | |
const failed = [] | |
for (const {repo, branch} of repoBranches) { | |
core.info(`Creating PR for ${repo.html_url}/tree/${branch.name}`) | |
if (process.env.DRY_RUN == 'true') { | |
core.info(`Skipping PR creation because this is a dry run`) | |
continue | |
} | |
try { | |
const pr = await github.rest.pulls.create({ | |
owner: repo.owner.login, | |
repo: repo.name, | |
head: branch.name, | |
base: repo.default_branch, | |
title: `ci: ${branch.name}`, | |
body: `This PR was created automatically by the @web3-bot as a part of the [Unified CI](https://github.com/ipdxco/unified-github-workflows) project.` | |
}) | |
core.info(`${pr.data.html_url} created successfully`) | |
} catch(error) { | |
core.error(`Couldn't create a PR for ${repo.html_url}/tree/${branch.name}, got: ${error}`) | |
failed.push({repo, branch}) | |
} | |
} | |
if (failed.length != 0) { | |
throw new Error(`Failed to create ${failed.length} PRs`) | |
} |