diff --git a/.github/workflows/renovate_prune.yml b/.github/workflows/renovate_prune.yml new file mode 100644 index 00000000..531b3c37 --- /dev/null +++ b/.github/workflows/renovate_prune.yml @@ -0,0 +1,94 @@ +name: "Prune Renovate Branches" + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * *' # daily at 03:00 UTC + pull_request: + types: [closed] # also run when any PR is closed (makes pruning near-real-time) + +permissions: + contents: write # required to delete branch refs + pull-requests: read + +jobs: + prune: + runs-on: ubuntu-latest + concurrency: + group: prune-renovate-branches + cancel-in-progress: true + + env: + BRANCH_PREFIX: "renovate/" # adjust if you use a different prefix + + steps: + - name: Prune renovate branches + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const branchPrefix = (process.env.BRANCH_PREFIX || 'renovate/').replace(/^refs\/heads\//, ''); + const refPrefix = `heads/${branchPrefix}`; // e.g. "heads/renovate/" + + core.info(`Searching for refs with prefix: ${refPrefix}`); + + // List matching refs (branches) under the prefix + let refs; + try { + refs = await github.rest.git.listMatchingRefs({ + owner, + repo, + ref: refPrefix + }); + } catch (err) { + core.info(`No matching refs or API error: ${err.message}`); + refs = { data: [] }; + } + + for (const r of refs.data) { + const fullRef = r.ref; // "refs/heads/renovate/..." + const branchName = fullRef.replace('refs/heads/', ''); + core.info(`Evaluating branch: ${branchName}`); + + // Find PRs for this branch (head = "owner:branch") + const prs = await github.rest.pulls.list({ + owner, + repo, + head: `${owner}:${branchName}`, + state: 'all', + per_page: 100 + }); + + let shouldDelete = false; + if (!prs.data || prs.data.length === 0) { + core.info(`No PRs found for ${branchName} — marking for deletion.`); + shouldDelete = true; + } else { + // If none of the PRs are open, safe to delete + const hasOpen = prs.data.some(p => p.state === 'open'); + if (!hasOpen) { + core.info(`All PRs for ${branchName} are closed — marking for deletion.`); + shouldDelete = true; + } else { + core.info(`Open PR(s) exist for ${branchName} — skipping deletion.`); + } + } + + if (shouldDelete) { + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${branchName}` + }); + core.info(`Deleted branch: ${branchName}`); + } catch (delErr) { + core.warning(`Failed to delete ${branchName}: ${delErr.message}`); + } + } + } + + - name: Done + run: echo "Prune run completed."