Commit ef4660f
ci: sweep Actions caches belonging to closed PRs (#5009)
## What it does
Adds `sweep-closed-prs` to `cache-cleanup.yml`: a daily (`17 4 * * *`)
plus on-demand job that deletes caches on `refs/pull/N/merge` once the
PR is no longer open.
Those caches live on a ref only that PR can read. When it closes they
become permanently unreachable but keep holding budget for up to the
full 7-day retention, which is not configurable.
## Why scheduled, not `pull_request: closed`
The obvious trigger cannot work, which is why #5008 pulled the original
version rather than shipping it: for a `pull_request` event whose head
is a fork, GitHub issues a **read-only** `GITHUB_TOKEN`, and a
`permissions:` block can only lower that ceiling, never raise it. `gh
cache delete` returns 403 and the job fails. **44 of the last 60 closed
PRs here are from forks** (measured 2026-09-10).
`pull_request_target` would get a writable token and is the
documented-safe shape for a job that checks nothing out. It's
deliberately not used: a schedule needs no privileged trigger at all, is
idempotent, and **self-heals** — it picks up PRs closed while the
workflow was broken, renamed, or disabled, which an event-driven job
misses forever.
The cost is latency, and it is not quite free: while the repo is over
its allowance, dead entries compete with main's live ones for LRU until
the sweep runs. If that bites, the job is idempotent and cheap enough to
also hang off the existing `workflow_run` trigger, with the schedule
kept as the backstop. Not done here; see follow-ups.
## Measured premise (2026-09-10, 25 days after #5008)
The original draft held this PR until it could be shown that anything
still accumulates on PR refs after #5008's save-on-main split. It does:
```
PR-ref caches: 20 entries, 6881 MiB
repo usage: 23 entries, 9.88 GiB of 10 GiB
#5458 MERGED 1720 MiB ← unreachable, swept by this job
#5462 OPEN 396 MiB
#5539 OPEN 1323 MiB
#5542 OPEN 1720 MiB
#5547 OPEN 1720 MiB
```
That is past the "> 2 GiB, or any single Qt-sized entry → merge"
threshold the draft set for itself. The "PRs should restore rather than
write" hypothesis did not hold: each open PR carries its own copy of the
same 1,297 MiB Qt key main holds, because at the allowance LRU evicts
main's copy between runs and the next PR re-saves it. This job reclaims
the closed-PR leg of that loop; the loop itself is a ci.yml matter
(follow-ups).
## Failure behaviour
| case | behaviour |
|---|---|
| PR state unresolvable | Skipped and retried next run — a transient API
error must not become data loss. Counted, and surfaced as a
`::warning::` with gh's stderr so a persistent cause (missing scope,
rate limit) cannot hide |
| `--limit 500` truncation | Emits `::warning::`, because silent
truncation looks exactly like "nothing to clean" |
| Individual delete fails / LRU took it first | Deleted by id like
prune-main; a 404 is logged, does not abort the loop, and does not count
toward `freed` |
| Dispatch overlaps the cron | `concurrency` group queues the second run
|
## Cannot be exercised on this PR
`schedule` and `workflow_dispatch` only fire from the **default
branch**. First real run is after merge: trigger it via
`workflow_dispatch` immediately rather than waiting for 04:17, and read
the log before trusting it. That first run is also what confirms the
`pull-requests: read` grant is sufficient for `gh pr view` under
`GITHUB_TOKEN`.
## Not addressed here
- Caches on **tag refs** (`refs/tags/v*`) from the release workflows are
swept by neither job. Low volume; noted so it isn't silently forgotten.
- The open-PR duplication loop above. The durable fix is giving the
dependency caches (Qt, FFTW3, DeepFilterNet3, qtkeychain) the same
restore-on-PR / save-on-main split #5008 gave the compiler caches;
`install-qt-action`'s built-in cache has no restore-only mode and needs
its own decision.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent df0520a commit ef4660f
1 file changed
Lines changed: 162 additions & 14 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
48 | 40 | | |
49 | 41 | | |
50 | 42 | | |
51 | 43 | | |
52 | 44 | | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
53 | 50 | | |
54 | 51 | | |
55 | 52 | | |
56 | 53 | | |
57 | 54 | | |
58 | 55 | | |
59 | | - | |
60 | | - | |
61 | | - | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
62 | 65 | | |
63 | 66 | | |
64 | 67 | | |
| 68 | + | |
65 | 69 | | |
66 | 70 | | |
67 | 71 | | |
| 72 | + | |
| 73 | + | |
68 | 74 | | |
69 | 75 | | |
70 | 76 | | |
| |||
163 | 169 | | |
164 | 170 | | |
165 | 171 | | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
0 commit comments