Skip to content

Delete branches

Delete branches #204

name: Delete branches
on:
workflow_dispatch:
inputs:
branch:
description: "Branch to delete"
required: false
dry-run:
description: "Whether to run in dry run mode"
required: false
default: 'false'
schedule:
- cron: '10 8 * * *' # https://crontab.guru/#10_8_*_*_*
jobs:
delete-branches:
name: Delete branches
runs-on: ubuntu-latest
steps:
- name: Delete branches
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 branches to delete`)
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} can be deleted`)
const {data: pulls} = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: item.owner.login,
repo: item.name,
commit_sha: branch.commit.sha
})
const openPulls = pulls.filter(pull => pull.state == 'open')
if (pulls.length != 0 && openPulls.length == 0) {
core.info(`${item.html_url}/tree/${branch.name} has ${pulls.length} PRs, but none are open, deleting`)
repoBranches.push({repo: item, branch})
} else {
core.info(`${item.html_url}/tree/${branch.name} has ${pulls.length} PRs, ${openPulls.length} are open, skipping`)
}
}
}
core.info(`Attempting to delete branches`)
const failed = []
for (const {repo, branch} of repoBranches) {
core.info(`Deleting ${repo.html_url}/tree/${branch.name}`)
if (process.env.DRY_RUN == 'true') {
core.info(`Skipping branch deletion because this is a dry run`)
continue
}
try {
await github.rest.git.deleteRef({
owner: repo.owner.login,
repo: repo.name,
ref: `heads/${branch.name}`
})
core.info(`${repo.html_url}/tree/${branch.name} deleted successfully`)
} catch(error) {
core.error(`Couldn't delete ${repo.html_url}/tree/${branch.name}, got: ${error}`)
failed.push({repo, branch})
}
}
if (failed.length != 0) {
throw new Error(`Failed to delete ${failed.length} branches`)
}