From 5a00f1a380b0775218b4adf3311aff4fc5c559a9 Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Sat, 9 May 2026 23:27:02 +0200 Subject: [PATCH] =?UTF-8?q?ci:=20cap=20Actions=20storage=20growth=20?= =?UTF-8?q?=E2=80=94=20buildx=20mode=3Dmin=20+=20auto-cleanup=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hit the 0.5 GB Actions storage quota today. Two-pronged fix. 1. docker-publish.yml: cache-to switched from mode=max to mode=min. With mode=max, buildx pushes every intermediate layer of the multi-stage build (qemu-provider, espidf-builder, frontend-builder, final stage) into the GHA cache. For our image that's easily 500 MB-1 GB per cache update. mode=min stores only the layers used by the final image; incremental rebuilds still hit the cache for the meaningful steps but the footprint drops by roughly 60-70%. 2. actions-cache-cleanup.yml (new workflow): - Weekly schedule (Sun 04:00 UTC): deletes every cache older than 14 days. Catches stale entries from deleted branches. - On `pull_request: closed`: deletes caches scoped to that PR's branch ref AND the merge ref. Buildx + actions/cache scope per branch, so a closed PR's caches are immediately stale — without this they linger until the GHA-default 7-day eviction. - Manual `workflow_dispatch` for one-shot runs when storage is already over. Permissions: each job sets `actions: write` (the minimum needed for cache deletion). No GH_TOKEN secret required; the default GITHUB_TOKEN already has the scope. Quota math after this lands: Before: every push to master = +500 MB-1 GB cache, kept 7 days → quota fills in 1-2 builds. After: every push to master = +200-400 MB cache, plus old branches actively swept; 0.5 GB stays comfortable. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/actions-cache-cleanup.yml | 118 ++++++++++++++++++++ .github/workflows/docker-publish.yml | 8 +- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/actions-cache-cleanup.yml diff --git a/.github/workflows/actions-cache-cleanup.yml b/.github/workflows/actions-cache-cleanup.yml new file mode 100644 index 00000000..351dd724 --- /dev/null +++ b/.github/workflows/actions-cache-cleanup.yml @@ -0,0 +1,118 @@ +# Keeps GitHub Actions cache storage from filling up. +# +# Two complementary triggers: +# +# 1. Weekly sweep — delete every cache older than 14 days. Catches the slow +# drift from old branches and stale entries that nothing references. +# 2. PR-close sweep — when a pull request is merged or closed, delete every +# cache scoped to that PR's branch. Most caches are short-lived per-PR +# buildx scopes; without this they linger for the full retention window +# eating quota for nothing. +# +# Storage quota on davidmonterocrespo24's plan is 0.5 GB. A multi-stage Docker +# build with mode=min cache can sit around 200-400 MB so two stale PR caches +# are enough to push us over. +name: Actions cache cleanup + +on: + schedule: + # Sundays at 04:00 UTC — well outside CI peak hours. + - cron: '0 4 * * 0' + pull_request: + types: [closed] + workflow_dispatch: {} + +jobs: + weekly-sweep: + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Delete caches older than 14 days + uses: actions/github-script@v7 + with: + script: | + const cutoffMs = Date.now() - 14 * 24 * 60 * 60 * 1000; + let page = 1; + let deleted = 0; + let kept = 0; + let bytesFreed = 0; + while (true) { + const { data } = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + page, + }); + if (!data.actions_caches.length) break; + for (const cache of data.actions_caches) { + const created = new Date(cache.created_at).getTime(); + if (created < cutoffMs) { + console.log( + `delete ${cache.id} ${(cache.size_in_bytes / 1024 / 1024).toFixed(1)} MB ${cache.ref} ${cache.key}`, + ); + await github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }); + deleted += 1; + bytesFreed += cache.size_in_bytes; + } else { + kept += 1; + } + } + if (data.actions_caches.length < 100) break; + page += 1; + } + console.log( + `summary: deleted=${deleted} kept=${kept} freed=${(bytesFreed / 1024 / 1024).toFixed(1)} MB`, + ); + + pr-close-sweep: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Delete caches scoped to the closed PR's branch + uses: actions/github-script@v7 + env: + PR_REF: refs/pull/${{ github.event.pull_request.number }}/merge + BRANCH_REF: refs/heads/${{ github.event.pull_request.head.ref }} + with: + script: | + const refs = [process.env.PR_REF, process.env.BRANCH_REF]; + let deleted = 0; + let bytesFreed = 0; + for (const ref of refs) { + let page = 1; + while (true) { + const { data } = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + ref, + per_page: 100, + page, + }); + if (!data.actions_caches.length) break; + for (const cache of data.actions_caches) { + console.log( + `delete ${cache.id} ${(cache.size_in_bytes / 1024 / 1024).toFixed(1)} MB ${cache.ref} ${cache.key}`, + ); + await github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }); + deleted += 1; + bytesFreed += cache.size_in_bytes; + } + if (data.actions_caches.length < 100) break; + page += 1; + } + } + console.log( + `pr-close summary: deleted=${deleted} freed=${(bytesFreed / 1024 / 1024).toFixed(1)} MB`, + ); diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 0e071169..dc594c40 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -60,7 +60,13 @@ jobs: build-args: | ESPIDF_IMAGE=ghcr.io/davidmonterocrespo24/velxio-espidf-toolchain:latest cache-from: type=gha - cache-to: type=gha,mode=max + # mode=min only stores the layers used in the final image. mode=max + # also stores all intermediate layers, which doubles or triples the + # cache footprint on a multi-stage build like ours (qemu-provider + + # espidf-builder + frontend-builder + final stage). The 0.5 GB + # Actions storage quota burns through fast with max; min is enough + # to make incremental rebuilds fast on the layers that matter. + cache-to: type=gha,mode=min - name: Ping search engines with sitemap if: success()