Skip to content

Prune old deployments #1026

Prune old deployments

Prune old deployments #1026

name: Prune old deployments
on:
workflow_run:
workflows: [CI / CD]
types: [completed]
schedule:
# Weekly cleanup on Sundays at 03:00 UTC
- cron: '0 3 * * 0'
workflow_dispatch:
# QNBS-v3: top-level read-only; deployments:write scoped to job (CodeQL Token-Permissions).
permissions:
contents: read
jobs:
prune:
name: Keep latest 3 deployments
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
deployments: write
steps:
- name: Delete stale deployments β€” keep latest 3 per environment
# QNBS-v3: v9.0.0 uses node24 β€” v7 was node20 (deprecated June 2026)
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const KEEP = 3;
// QNBS-v3: Paginate ALL deployments (not just one environment) β€” Vercel creates
// "Production" and "Preview" environments that accumulate independently
// of the "github-pages" environment; the old filter missed them entirely.
const allDeployments = await github.paginate(
github.rest.repos.listDeployments,
{
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
},
);
// Group by environment, sort each group newest-first, keep the first KEEP
const byEnv = {};
for (const dep of allDeployments) {
const env = dep.environment || 'unknown';
(byEnv[env] ??= []).push(dep);
}
let totalDeleted = 0;
let totalFailed = 0;
for (const [env, deps] of Object.entries(byEnv)) {
const sorted = deps.sort((a, b) => b.id - a.id);
const toDelete = sorted.slice(KEEP);
if (toDelete.length === 0) {
core.info(`[${env}] Nothing to prune β€” only ${deps.length} deployment(s).`);
continue;
}
core.info(`[${env}] ${deps.length} total β†’ pruning ${toDelete.length}, keeping ${KEEP} latest.`);
let deleted = 0;
let failed = 0;
for (const dep of toDelete) {
// A deployment must be inactive before it can be deleted
try {
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: dep.id,
state: 'inactive',
});
} catch (e) {
core.warning(`[${env}] Could not inactivate ${dep.id}: ${e.message}`);
}
try {
await github.rest.repos.deleteDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: dep.id,
});
deleted++;
} catch (e) {
core.warning(`[${env}] Could not delete ${dep.id}: ${e.message}`);
failed++;
}
}
core.info(`[${env}] Done: ${deleted} deleted, ${failed} failed.`);
totalDeleted += deleted;
totalFailed += failed;
}
core.info(`All environments: ${totalDeleted} deleted, ${totalFailed} failed.`);