This is an automated email from the ASF dual-hosted git repository. sjanc pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mynewt-core.git
commit 757a623ee43becff178f3ac0c40352b63a357799 Author: Szymon Janc <[email protected]> AuthorDate: Thu Oct 15 19:04:13 2026 +0200 ci: Remove superseded caches at end of build workflows Cache cleanup workflow failed on every run, since github.paginate() already returns flat list of caches for this endpoint and map function extracting actions_caches from response returned undefined for every page: Unhandled error: TypeError: Cannot read properties of undefined (reading 'key') As result no superseded caches were removed and caches saved by master builds quickly filled up repository cache quota. Instead of fixing separate workflow triggered by completed builds and by schedule, remove superseded caches in last job of each workflow that saves caches, right after caches are saved. Only that job has write access to caches, jobs building code still have read-only token. Cleanup logic is moved to composite action shared by both workflows. Entries of ccache are now ordered by run ID and attempt from key instead of creation time, so that entry saved by older run that completed after newer one never replaces entry saved by newer run. Assisted-by: Claude:claude-opus-5-5 --- .github/actions/cache-cleanup/action.yml | 107 ++++++++++++++++++++++++++++ .github/workflows/build.yml | 20 ++++++ .github/workflows/build_bootloader_main.yml | 18 +++++ .github/workflows/cache_cleanup.yml | 101 -------------------------- 4 files changed, 145 insertions(+), 101 deletions(-) diff --git a/.github/actions/cache-cleanup/action.yml b/.github/actions/cache-cleanup/action.yml new file mode 100644 index 000000000..a1f6b4561 --- /dev/null +++ b/.github/actions/cache-cleanup/action.yml @@ -0,0 +1,107 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +name: Remove superseded caches +description: > + Remove caches superseded by newer ones. Caches are saved only from + master branch with unique keys (e.g. ccache-<job>-<run_id>-<attempt>) + and only newest entry of each kind is used by later runs, so older + entries only take space from repository cache quota. Should be used + from job run after all jobs saving caches are completed. Requires + actions: write permission. + +runs: + using: composite + steps: + - uses: actions/github-script@v9 + with: + script: | + // Kind of cache is identified by all capture groups of + // matching pattern. Entries of ccache are ordered by run ID + // and attempt from key, so that entry saved by older run + // completed after newer one never replaces newer entry, + // other caches by creation time. + const families = [ + { + re: /^(ccache-.+)-(\d+)-(\d+)$/, + name: (m) => m[1], + order: (m) => [Number(m[2]), Number(m[3])], + }, + { + re: /^(newt-repos-[^-]+)-[0-9a-f]+$/, + name: (m) => m[1], + }, + { + re: /^(newt-[^-]+)-[0-9a-f]{40}-(.+)$/, + name: (m) => `${m[1]}-${m[2]}`, + }, + ]; + + const caches = await github.paginate( + github.rest.actions.getActionsCacheList, + { ...context.repo, ref: 'refs/heads/master', per_page: 100 }); + + const newer = (a, b) => { + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return a[i] > b[i]; + } + } + return false; + }; + + const newest = new Map(); + const stale = []; + for (const cache of caches) { + for (const family of families) { + const m = cache.key.match(family.re); + if (!m) { + continue; + } + const name = family.name(m); + const order = family.order ? + family.order(m) : [Date.parse(cache.created_at)]; + const current = newest.get(name); + if (!current) { + newest.set(name, { cache, order }); + } else if (newer(order, current.order)) { + stale.push(current.cache); + newest.set(name, { cache, order }); + } else { + stale.push(cache); + } + break; + } + } + + for (const cache of stale) { + core.info(`Removing ${cache.key} (${cache.size_in_bytes} bytes)`); + try { + await github.rest.actions.deleteActionsCacheById({ + ...context.repo, + cache_id: cache.id, + }); + } catch (e) { + // Cache may be already removed by GitHub or other run + if (e.status !== 404) { + throw e; + } + } + } + core.info(`Kept ${newest.size} caches, removed ${stale.length}`); diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dc6fb949..a2c7da3b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -407,3 +407,23 @@ jobs: uses: ./.github/actions/ccache-save with: key: unit-tests + + # Jobs save caches with unique keys, remove entries superseded by + # caches saved in this run. Separate job, so that only it has write + # access to caches and none of jobs building code does. + cache-cleanup: + name: Remove superseded caches + needs: [newt, prepare, blinky, bootloader, apps, targets, mtest-apps, gcc-target, unit-tests] + if: >- + !cancelled() && github.ref == 'refs/heads/master' && + github.event_name != 'pull_request' + runs-on: ubuntu-slim + permissions: + actions: write + contents: read + steps: + - uses: actions/checkout@v7 + with: + sparse-checkout: .github/actions + - name: Remove superseded caches + uses: ./.github/actions/cache-cleanup diff --git a/.github/workflows/build_bootloader_main.yml b/.github/workflows/build_bootloader_main.yml index ad1d0a4b7..bbe9b6c3e 100644 --- a/.github/workflows/build_bootloader_main.yml +++ b/.github/workflows/build_bootloader_main.yml @@ -65,3 +65,21 @@ jobs: uses: ./.github/actions/ccache-save with: key: bootloader-main + + # Separate job, so that only it has write access to caches + cache-cleanup: + name: Remove superseded caches + needs: bootloader + if: >- + !cancelled() && github.ref == 'refs/heads/master' && + github.event_name != 'pull_request' + runs-on: ubuntu-slim + permissions: + actions: write + contents: read + steps: + - uses: actions/checkout@v7 + with: + sparse-checkout: .github/actions + - name: Remove superseded caches + uses: ./.github/actions/cache-cleanup diff --git a/.github/workflows/cache_cleanup.yml b/.github/workflows/cache_cleanup.yml deleted file mode 100644 index 99f2d437c..000000000 --- a/.github/workflows/cache_cleanup.yml +++ /dev/null @@ -1,101 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -name: Cache cleanup - -on: - # Remove caches superseded by master build right after it completes, - # daily run handles caches saved by other workflows - workflow_run: - workflows: [Build] - types: [completed] - schedule: - - cron: '30 3 * * *' - workflow_dispatch: - -permissions: - actions: write - -concurrency: - group: ${{ github.workflow }} - -jobs: - cleanup: - name: Remove superseded caches - # Build runs on push only for master, pull request runs are ignored - if: >- - github.event_name != 'workflow_run' || - github.event.workflow_run.event == 'push' - runs-on: ubuntu-slim - steps: - - uses: actions/github-script@v9 - with: - script: | - // Caches created by master branch builds are saved with - // unique keys (e.g. ccache-<job>-<run_id>-<attempt>), only - // newest entry of each kind is used by later runs. Remove - // older entries to keep total cache size below limit. Kind - // is identified by all capture groups of matching pattern. - const families = [ - { re: /^(ccache-.+)-\d+-\d+$/ }, - { re: /^(newt-repos-[^-]+)-[0-9a-f]+$/ }, - { re: /^(newt-[^-]+)-[0-9a-f]{40}-(.+)$/ }, - ]; - - const caches = await github.paginate( - github.rest.actions.getActionsCacheList, - { ...context.repo, ref: 'refs/heads/master', per_page: 100 }, - (response) => response.data.actions_caches); - - const newest = new Map(); - const stale = []; - for (const cache of caches) { - const family = families - .map((f) => cache.key.match(f.re)) - .find((m) => m); - if (!family) { - continue; - } - const name = family.slice(1).join('-'); - const current = newest.get(name); - if (!current) { - newest.set(name, cache); - } else if (new Date(cache.created_at) > new Date(current.created_at)) { - stale.push(current); - newest.set(name, cache); - } else { - stale.push(cache); - } - } - - for (const cache of stale) { - core.info(`Removing ${cache.key} (${cache.size_in_bytes} bytes)`); - try { - await github.rest.actions.deleteActionsCacheById({ - ...context.repo, - cache_id: cache.id, - }); - } catch (e) { - // Cache may be already evicted by GitHub - if (e.status !== 404) { - throw e; - } - } - } - core.info(`Kept ${newest.size} caches, removed ${stale.length}`);
