|
| 1 | +name: Cache Maintenance |
| 2 | + |
| 3 | +on: |
| 4 | + schedule: |
| 5 | + - cron: '30 3 * * 0' |
| 6 | + workflow_dispatch: |
| 7 | + inputs: |
| 8 | + keep_per_key: |
| 9 | + description: How many recent caches to keep for each normalized key |
| 10 | + type: choice |
| 11 | + default: '2' |
| 12 | + options: |
| 13 | + - '1' |
| 14 | + - '2' |
| 15 | + |
| 16 | +permissions: |
| 17 | + actions: write |
| 18 | + contents: read |
| 19 | + |
| 20 | +jobs: |
| 21 | + prune-caches: |
| 22 | + runs-on: ubuntu-latest |
| 23 | + env: |
| 24 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 25 | + KEEP_PER_KEY: ${{ inputs.keep_per_key || '2' }} |
| 26 | + |
| 27 | + steps: |
| 28 | + - name: List caches |
| 29 | + run: gh cache list --limit 1000 --json createdAt,id,key,lastAccessedAt,ref,sizeInBytes > caches.json |
| 30 | + |
| 31 | + - name: Select stale caches |
| 32 | + shell: bash |
| 33 | + run: | |
| 34 | + node <<'NODE' |
| 35 | + const fs = require('fs'); |
| 36 | +
|
| 37 | + const keepCount = Number(process.env.KEEP_PER_KEY || '2'); |
| 38 | + const caches = JSON.parse(fs.readFileSync('caches.json', 'utf8')); |
| 39 | + const groups = new Map(); |
| 40 | +
|
| 41 | + const normalizeKey = (key) => key.replace(/-[0-9a-f]{32,}$/i, ''); |
| 42 | + const score = (cache) => new Date(cache.lastAccessedAt || cache.createdAt || 0).getTime(); |
| 43 | +
|
| 44 | + for (const cache of caches) { |
| 45 | + const groupKey = normalizeKey(cache.key); |
| 46 | + if (!groups.has(groupKey)) { |
| 47 | + groups.set(groupKey, []); |
| 48 | + } |
| 49 | + groups.get(groupKey).push(cache); |
| 50 | + } |
| 51 | +
|
| 52 | + const stale = []; |
| 53 | + const summary = []; |
| 54 | +
|
| 55 | + for (const [groupKey, items] of groups.entries()) { |
| 56 | + items.sort((a, b) => score(b) - score(a)); |
| 57 | + const keep = items.slice(0, keepCount); |
| 58 | + const remove = items.slice(keepCount); |
| 59 | +
|
| 60 | + summary.push( |
| 61 | + `${groupKey}: keep ${keep.length}, delete ${remove.length}, total ${items.length}`, |
| 62 | + ); |
| 63 | +
|
| 64 | + stale.push(...remove); |
| 65 | + } |
| 66 | +
|
| 67 | + fs.writeFileSync('stale-cache-ids.txt', stale.map((cache) => String(cache.id)).join('\n')); |
| 68 | + fs.writeFileSync( |
| 69 | + process.env.GITHUB_STEP_SUMMARY, |
| 70 | + [ |
| 71 | + '# Cache Maintenance', |
| 72 | + '', |
| 73 | + `Keep per normalized key: ${keepCount}`, |
| 74 | + 'Normalized key rule: strip trailing hex hash like `-f27f...`', |
| 75 | + '', |
| 76 | + `Total caches scanned: ${caches.length}`, |
| 77 | + `Caches selected for deletion: ${stale.length}`, |
| 78 | + '', |
| 79 | + '## Groups', |
| 80 | + ...summary.map((line) => `- ${line}`), |
| 81 | + ].join('\n'), |
| 82 | + ); |
| 83 | +
|
| 84 | + console.log(`Caches scanned: ${caches.length}`); |
| 85 | + console.log(`Caches selected for deletion: ${stale.length}`); |
| 86 | + NODE |
| 87 | +
|
| 88 | + - name: Delete stale caches |
| 89 | + shell: bash |
| 90 | + run: | |
| 91 | + if [ ! -s stale-cache-ids.txt ]; then |
| 92 | + echo "No stale caches to delete" |
| 93 | + exit 0 |
| 94 | + fi |
| 95 | +
|
| 96 | + while IFS= read -r cache_id; do |
| 97 | + if [ -n "$cache_id" ]; then |
| 98 | + echo "Deleting cache $cache_id" |
| 99 | + gh cache delete "$cache_id" |
| 100 | + fi |
| 101 | + done < stale-cache-ids.txt |
0 commit comments