Skip to content

Hourly Seed PR Cleanup #1114

Hourly Seed PR Cleanup

Hourly Seed PR Cleanup #1114

name: Hourly Seed PR Cleanup
on:
schedule:
- cron: "17 * * * *"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
cleanup-seed-pr:
runs-on: ubuntu-latest
steps:
- name: Close non-seed PRs hourly; close seed PRs only after 12h
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const seedLabel = "lab-seed-pr";
const now = new Date();
const { data: repoInfo } = await github.rest.repos.get({ owner, repo });
const defaultBranch = repoInfo.default_branch;
const deleteSameRepoBranch = async (pr, why) => {
const headRef = pr?.head?.ref;
const headRepoFullName = pr?.head?.repo?.full_name;
if (!headRef) {
core.warning(`PR #${pr.number}: missing head ref; cannot delete branch.`);
return;
}
if (headRef === defaultBranch) {
core.warning(`PR #${pr.number}: head ref is default branch (${defaultBranch}); skipping deletion.`);
return;
}
if (headRepoFullName !== `${owner}/${repo}`) {
core.info(`PR #${pr.number}: head branch is in a fork (${headRepoFullName}); cannot delete from this repo.`);
return;
}
const ref = `heads/${headRef}`;
try {
await github.rest.git.deleteRef({ owner, repo, ref });
core.info(`PR #${pr.number}: deleted branch ${ref}${why ? ` (${why})` : ""}.`);
} catch (e) {
const status = e?.status;
if (status === 404 || status === 422) {
core.info(`PR #${pr.number}: branch ${ref} already gone or cannot be deleted (${status}).`);
return;
}
core.warning(`PR #${pr.number}: could not delete ${ref}: ${e.message}`);
}
};
const pulls = await github.paginate(github.rest.pulls.list, {
owner, repo, state: "open", per_page: 100
});
for (const pr of pulls) {
const labels = (pr.labels || []).map(l => l.name);
const isSeed = labels.includes(seedLabel);
const created = new Date(pr.created_at);
const ageHours = (now - created) / (1000 * 60 * 60);
if (isSeed && ageHours < 12) continue;
const reason = isSeed
? "Auto-closed by hourly cleanup after 12h seed lifetime."
: "Auto-closed by hourly cleanup (non-seed PR).";
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: reason
});
await github.rest.pulls.update({
owner, repo, pull_number: pr.number, state: "closed"
});
await deleteSameRepoBranch(pr, "hourly-close");
}
// Hourly sweep: try to delete branches for recently-closed PRs too.
// This is a safety net for cases where the on-close workflow didn't run
// or branch deletion failed transiently.
const maxAgeHoursForClosedSweep = 48;
const closedPulls = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "closed",
per_page: 100,
sort: "updated",
direction: "desc"
});
for (const pr of closedPulls) {
const updated = new Date(pr.updated_at);
const ageHours = (now - updated) / (1000 * 60 * 60);
if (ageHours > maxAgeHoursForClosedSweep) break;
await deleteSameRepoBranch(pr, "hourly-sweep");
}