mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-06-04 14:49:35 +02:00
135 lines
4.6 KiB
YAML
Generated
135 lines
4.6 KiB
YAML
Generated
name: Delete merged branches
|
|
|
|
on:
|
|
schedule:
|
|
- cron: "0 2 * * *" # Run daily at 02:00 UTC
|
|
workflow_dispatch:
|
|
inputs:
|
|
dry_run:
|
|
description: "Only log branches that would be deleted (no deletion)"
|
|
type: boolean
|
|
default: true
|
|
|
|
permissions:
|
|
contents: write # required to delete branch refs
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
delete-merged-branches:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Delete branches of merged PRs
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
|
|
// dry_run is only set on workflow_dispatch; scheduled runs delete for real.
|
|
const dryRun = context.eventName === "workflow_dispatch"
|
|
? context.payload.inputs?.dry_run === "true" || context.payload.inputs?.dry_run === true
|
|
: false;
|
|
|
|
// Only look at PRs updated within this window on scheduled runs, so we don't
|
|
// re-scan the entire history every day. Raise this for an initial cleanup.
|
|
const lookbackDays = 30;
|
|
const cutoff = new Date();
|
|
cutoff.setDate(cutoff.getDate() - lookbackDays);
|
|
|
|
// Long-lived branches that must never be deleted (besides the default branch).
|
|
const protectedNames = new Set();
|
|
|
|
// Resolve the default branch once.
|
|
const { data: repoData } = await github.rest.repos.get({ owner, repo });
|
|
const defaultBranch = repoData.default_branch;
|
|
protectedNames.add(defaultBranch);
|
|
|
|
console.log(`Mode: ${dryRun ? "DRY RUN (no deletion)" : "LIVE"}`);
|
|
console.log(`Default branch: ${defaultBranch}`);
|
|
|
|
const candidates = new Set();
|
|
let page = 1;
|
|
let stop = false;
|
|
|
|
while (!stop) {
|
|
const { data: prs } = await github.rest.pulls.list({
|
|
owner,
|
|
repo,
|
|
state: "closed",
|
|
sort: "updated",
|
|
direction: "desc",
|
|
per_page: 100,
|
|
page,
|
|
});
|
|
|
|
if (prs.length === 0) break;
|
|
|
|
for (const pr of prs) {
|
|
// Sorted by updated desc: once we pass the cutoff we can stop paginating.
|
|
if (new Date(pr.updated_at) < cutoff) {
|
|
stop = true;
|
|
break;
|
|
}
|
|
|
|
if (!pr.merged_at) continue; // only merged PRs
|
|
if (!pr.head.repo) continue; // head fork was deleted
|
|
if (pr.head.repo.full_name !== pr.base.repo.full_name) continue; // skip forks
|
|
|
|
const branch = pr.head.ref;
|
|
if (protectedNames.has(branch)) continue; // default / kept branches
|
|
|
|
candidates.add(branch);
|
|
}
|
|
|
|
page++;
|
|
if (page > 50) break; // safety cap
|
|
}
|
|
|
|
console.log(`Found ${candidates.size} unique candidate branch(es) from merged PRs.`);
|
|
|
|
let deleted = 0;
|
|
let skipped = 0;
|
|
|
|
for (const branch of candidates) {
|
|
// Confirm the branch still exists and isn't protected.
|
|
let branchData;
|
|
try {
|
|
const res = await github.rest.repos.getBranch({ owner, repo, branch });
|
|
branchData = res.data;
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
// Already deleted (e.g. auto-delete head branch) — nothing to do.
|
|
continue;
|
|
}
|
|
console.log(`Failed to inspect "${branch}": ${error.message}`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
if (branchData.protected) {
|
|
console.log(`Skipped "${branch}" (protected branch)`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
if (dryRun) {
|
|
console.log(`[dry-run] Would delete "${branch}"`);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await github.rest.git.deleteRef({ owner, repo, ref: `heads/${branch}` });
|
|
console.log(`Deleted "${branch}"`);
|
|
deleted++;
|
|
} catch (error) {
|
|
console.log(`Failed to delete "${branch}": ${error.message}`);
|
|
skipped++;
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
dryRun
|
|
? `Dry run complete. ${candidates.size} candidate(s) would be processed.`
|
|
: `Done. Deleted ${deleted} branch(es), skipped ${skipped}.`
|
|
);
|