-
-
Notifications
You must be signed in to change notification settings - Fork 87
69 lines (64 loc) · 3.59 KB
/
Copy pathcache-cleanup.yml
File metadata and controls
69 lines (64 loc) · 3.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
name: Clean up closed-PR caches
# Every PR-triggered run in ci.yml (node_modules, Turborepo, tsbuildinfo) saves its cache scoped to
# that PR's refs/pull/N/merge ref -- GitHub's own cache isolation, separate from the key string itself
# (two different PRs can share an identical key but still get two separate physical cache entries,
# because they're scoped to different refs). Once a PR closes, nothing can ever restore a cache scoped
# to its ref again -- and this repo auto-closes a red contributor PR one-shot (no reopen, no retry on
# the same PR), so most of those entries are written once and then permanently unreachable, just
# waiting on GitHub's passive 7-day-unused eviction. Confirmed live before adding this workflow: repo
# cache usage sat at ~10.7GB of the 10GB budget, with closed-PR-scoped node_modules caches alone
# accounting for the large majority of that -- crowding out the much smaller, much more useful
# Turborepo/tsbuildinfo caches for LRU survival.
#
# BATCHED, not per-event (queue-pressure fix, 2026-07-24): the original pull_request_target:[closed]
# trigger meant one queued runner per closed PR -- at this repo's one-shot-gate volume that was dozens
# of runs a day competing with real CI for the org's concurrent-runner cap during exactly the bursts
# when CI is most backed up. A cache entry lingering a few hours costs nothing (the 10GB budget has
# ample headroom between sweeps; LRU only bites near the cap), so one 6-hourly sweep that deletes every
# closed-PR-scoped entry replaces N per-close runs with 4/day flat, independent of PR volume.
on:
schedule:
- cron: "40 */6 * * *"
workflow_dispatch:
permissions:
actions: write
pull-requests: read
concurrency:
group: cache-cleanup-sweep
cancel-in-progress: false
jobs:
cleanup:
name: Delete closed PRs' caches
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Scoped by ref, not by key prefix (e.g. "npm-fork-") -- ANY cache entry scoped to a closed PR's
# ref (node_modules, Turborepo, tsbuildinfo, trusted or fork) is equally unreachable dead weight,
# regardless of which of ci.yml's cache families wrote it. PR state is checked per unique PR
# number (never inferred from cache age) so an idle-but-open PR -- maintainer branches routinely
# sit for hours between pushes -- never loses a still-restorable cache.
- name: Sweep caches scoped to closed PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh api "repos/${{ github.repository }}/actions/caches?per_page=100" --paginate \
--jq '.actions_caches[] | select(.ref // "" | test("^refs/pull/[0-9]+/merge$")) | "\(.id) \(.ref)"' > caches.txt || true
if [ ! -s caches.txt ]; then
echo "No PR-scoped caches found."
exit 0
fi
deleted=0
while read -r pr; do
state=$(gh api "repos/${{ github.repository }}/pulls/$pr" --jq .state 2>/dev/null || echo "unknown")
if [ "$state" != "closed" ]; then
continue
fi
while read -r id; do
echo "Deleting cache $id (refs/pull/$pr/merge)"
gh api -X DELETE "repos/${{ github.repository }}/actions/caches/$id" \
|| echo "::warning::Failed to delete cache $id (may already be gone)"
deleted=$((deleted + 1))
done < <(awk -v ref="refs/pull/$pr/merge" '$2 == ref {print $1}' caches.txt)
done < <(awk '{print $2}' caches.txt | sed -E 's#refs/pull/([0-9]+)/merge#\1#' | sort -un)
echo "Swept $deleted cache entr(ies) across closed PRs."