Install
$ agentstack add skill-stealth-factory-skills-vercel-preview-cleanup ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Vercel preview cleanup
Authors a GitHub Actions workflow that removes a branch's preview deployments once the branch is gone. This skill installs workflows; it never deletes anything itself. At runtime there is no agent and no model call — deterministic bash + curl only.
Verified API detail (endpoints, pagination, rate limits, retention) lives in [reference/vercel-api.md](./reference/vercel-api.md) — read it before changing the delete logic.
Step 0 — prerequisites (check before installing)
This skill is downstream of branch deletion. If branches are never deleted, the delete event never fires and this workflow looks broken when it isn't.
delete_branch_on_mergeis on:
gh api repos/{owner}/{repo} --jq .delete_branch_on_merge → if false, install [branch-cleanup](../branch-cleanup/SKILL.md) first (it enables this and adds the closed-unmerged + orphan-sweep paths).
- A branch-cleanup workflow exists (
.github/workflows/— closed-unmerged path).
Without it, only merged branches get deleted; abandoned ones linger forever and so do their previews.
Install branch-cleanup first. It is the prerequisite, not an optional companion.
> Neon's Vercel integration reaps preview database branches when the git branch > disappears — no code needed here. It is another reason timely branch deletion matters.
The problem this solves
Vercel creates a new immutable deployment per push. The branch alias repoints to the newest; every older deployment stays live at its own URL. A branch with 15 pushes leaves 15 reachable deployments. Deleting the branch removes none of them. You must enumerate and delete all of them.
Why on: delete, not pull_request: closed
- Deployments are keyed to the branch, not the PR. A branch pushed without a PR still
produces deployments; a PR-close trigger never sees them.
deletefires however the branch went away — merge auto-delete, manual, or `gh pr
merge --delete-branch`.
- In stacked workflows PRs close and reopen during restacks. A PR-close trigger would
delete previews for stack entries still in active use.
delete event caveats (all handled in the templates)
| Caveat | Handling | | --- | --- | | Fires for tags too | every job gated on github.event.ref_type == 'branch' | | No branch filter supported (unlike push) | filtered in-job; expect noisy run history | | Workflow must exist on the default branch | documented in the caller; it runs the default-branch definition because the branch is already gone | | github.ref is useless here (resolves to default branch) | read github.event.ref | | github.event.ref is the bare name | do not strip a refs/heads/ prefix that isn't there | | Documented cap: not triggered when deleting >3 tags at once (branch equivalent unverified) | the reconciliation sweep is the backstop either way |
The four safety layers
Deleting the wrong thing here means destroying a production deployment, so the guards are layered and independent:
- The branch must be gone —
on: deleteguarantees it, and because
workflow_dispatch does not, branch mode explicitly verifies the branch is absent before deleting anything. (Without that check a dispatch could wipe a live branch's previews, including the aliased one.)
- Include pattern
*/*(default) — only branches containing a/are cleaned, so
flat trunks (main, master, develop, staging, production) can never match.
- Protected denylist —
release/*andhotfix/*do contain a slash and would
pass layer 2, so they're denied explicitly. Layer 2 alone is not sufficient.
target != "production"in code — the hard guard, applied to every listed
deployment. ⚠️ Preview deployments carry target: null, not "preview" — selecting .target == "preview" matches zero rows and silently deletes nothing.
Both guards are re-applied per-candidate in the sweep too, not just on the event path — and because deletes are paced at ~3 s each, the sweep re-verifies each ref is still absent immediately before deleting rather than trusting the live-ref snapshot it built at the start (a branch recreated mid-sweep would otherwise lose its previews). The step summary reports counts per run (found / deleted / kept / failed) — enough to notice a filter that's excluding everything, though it does not list every kept ref.
Runtime behaviour
- List every deployment for the branch —
GET /v7/deployments, scoped byprojectId,
teamId, branch.
- Paginate to exhaustion.
pagination.nextis a millisecond timestamp passed
back as until; stop only when it is null. Partial pagination silently orphans the oldest deployments — the exact problem being solved.
- Filter: drop
target == "production"and already-deleted tombstones
(delete is a soft delete — they reappear in later lists).
- Cross-check each deployment's ref client-side before deleting — never trust the
server-side branch= filter alone.
- Delete each, paced at ~1 per 3 s. Vercel allows 200 deletes per 600 s per team;
a collapsing stack or a backfill will hit it. 200/404/410 all count as success.
Install
- Host [
templates/reusable-vercel-preview-cleanup.yml](./templates/reusable-vercel-preview-cleanup.yml)
once in the org's .github repo. Do not copy it per repo.
- Add [
templates/caller-vercel-preview-cleanup.yml](./templates/caller-vercel-preview-cleanup.yml)
to each repo's default branch (~30 lines). Replace YOUR-ORG and keep the uses: pinned to a full commit SHA (the template ships an obvious placeholder so it cannot silently run unpinned; a tag is mutable and can be moved). It forwards a delete-capable token; @main would let any change there take effect across every repo at once.
- Set per-repo
vars.VERCEL_PROJECT_IDS(comma-separated — a monorepo maps one git repo
to several Vercel projects) and vars.VERCEL_TEAM_ID; set secrets.VERCEL_TOKEN.
- Private repos: the hosting repo's Actions access settings must permit other org
repos to call its reusable workflows, or every caller fails.
- Test via
workflow_dispatchwithdry_run: true— you cannot teston: delete
from a feature branch.
Installation is idempotent: if a workflow already exists, diff it and offer an upgrade — never clobber local edits without showing them first. Validate generated YAML (actionlint if available, otherwise a YAML parse) before writing.
Fork PRs — the event path does NOT cover them
Be clear about this before installing. A fork PR's branch lives in the fork, so:
on: deletenever fires for it — there is no ref in your repo to delete.- It never appears in
repos/{owner}/{repo}/branches.
The only trigger that could clean a fork PR's previews the moment it closes is pull_request_target, and this skill deliberately does not use it. That trigger runs with your secrets in the base repo's context; combined with a VERCEL_TOKEN that can delete deployments, it is a supply-chain surface — and one actions/checkout added by a later edit turns it into remote code execution. Not worth it for this feature. If your org accepts that trade-off, the wiring is a pull_request_target: [closed] job gated to head.repo.full_name != github.repository; adopt it knowingly, and never check out fork code in it.
What you actually get instead:
| Fork PR state | Behaviour | | --- | --- | | Open | previews are protected — the sweep's "still live" set is repo branches ∪ every open PR head ref, so an in-review fork PR's previews are never deleted (without this union they'd look like orphans and be swept mid-review) | | Closed / merged | the ref is neither a branch nor an open PR head → the sweep collects it on its next run |
So forks are cleaned — just on the sweep's cadence (daily by default) rather than instantly. Two honest caveats:
- Latency. A fork PR's previews stay live until the next sweep. Tighten the cron if
that matters.
- Flat-named fork branches are never swept. GitHub's web editor defaults to
patch-1, which has no / and so fails the */* include guard by design. Those previews persist until Vercel's retention reaps them. Widening the pattern to catch them would also expose your own flat trunks — not a trade worth making.
Rolling out across many repos
- Pin the reusable workflow to a full commit SHA. It holds delete
permissions; @main means every repo silently picks up any change to it.
- Replace
YOUR-ORGin the caller'suses:with your organisation. VERCEL_PROJECT_IDSmisconfiguration is the top operational hazard. Point a repo
at a project belonging to a different repo and the sweep sees all of that project's refs as orphans. Verify the mapping per repo before enabling sweep_delete.
- Watch for failing sweeps. A daily sweep that errors is a red run nobody reads —
route the workflow's failure notifications somewhere a human sees.
Deliberately not built
- No cross-branch dependency check. Deployments are immutable and branch-scoped —
unlike branches, nothing else can depend on one. A later pass should not add a "does anything else use this deployment" guard; there is nothing to check.
- No
pull_request_target. It's the only trigger that could clean fork PR previews
on close, and it runs with secrets in the base repo's context. Declined deliberately — see "Fork PRs" above for what covers them instead.
- Retention is not a substitute. It's an independent backstop with its own floor
(last 10 project deployments, last 20 READY non-production, the latest preview of an active branch, …) and deploymentsToKeep is production-only. See [reference/vercel-api.md](./reference/vercel-api.md).
Backfill
The workflow only affects branches deleted from now on. For deployments already accumulated, run [scripts/vercel-backfill.sh](./scripts/vercel-backfill.sh) once — dry-run by default, same four guards, same pacing.
See also
- [
branch-cleanup](../branch-cleanup/SKILL.md) — the prerequisite. Deletes the
branches whose deletion triggers this skill. Install it first; it also works standalone for repos with no Vercel.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: stealth-factory
- Source: stealth-factory/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.