|
| 1 | +# Licensed to the Apache Software Foundation (ASF) under one or more |
| 2 | +# contributor license agreements. See the NOTICE file distributed with |
| 3 | +# this work for additional information regarding copyright ownership. |
| 4 | +# The ASF licenses this file to You under the Apache License, Version 2.0 |
| 5 | +# (the "License"); you may not use this file except in compliance with |
| 6 | +# the License. You may obtain a copy of the License at |
| 7 | +# |
| 8 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +# |
| 10 | +# Unless required by applicable law or agreed to in writing, software |
| 11 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +# See the License for the specific language governing permissions and |
| 14 | +# limitations under the License. |
| 15 | + |
| 16 | +# Manually services the `actions/cache` entries that hold the persisted |
| 17 | +# `~/.groovy/grapes/` directory used by the build/test workflows. |
| 18 | +# |
| 19 | +# Two modes: |
| 20 | +# |
| 21 | +# * `delete-entries` — wholesale removal of cache entries via |
| 22 | +# `gh cache delete`. Loses all cached JARs; the next regular workflow |
| 23 | +# run will repopulate from fresh resolutions. Use when a JAR itself |
| 24 | +# is corrupt or you want to start completely clean. |
| 25 | +# |
| 26 | +# * `scrub-ivydata` — restore the latest cache, delete only the |
| 27 | +# `ivydata-*.properties` files (Ivy's per-artifact resolver-state |
| 28 | +# cache, where transient "not found" responses get memoised), and |
| 29 | +# save back under a fresh key. Preserves all the actual JARs and |
| 30 | +# POMs. Use when CI is failing with `unresolved dependency` against |
| 31 | +# artifacts that *do* exist on Maven Central — the classic poisoned- |
| 32 | +# negative-cache symptom. |
| 33 | +# |
| 34 | +# After either mode, the next run of `Build and test` / `Build and test |
| 35 | +# for coverage` will pick up the cleaned (or absent) cache via the |
| 36 | +# `${{ runner.os }}-grape-` restore-keys prefix. |
| 37 | + |
| 38 | +name: Purge Grape cache |
| 39 | + |
| 40 | +on: |
| 41 | + workflow_dispatch: |
| 42 | + inputs: |
| 43 | + mode: |
| 44 | + description: 'What to do' |
| 45 | + required: true |
| 46 | + type: choice |
| 47 | + default: scrub-ivydata |
| 48 | + options: |
| 49 | + - scrub-ivydata |
| 50 | + - delete-entries |
| 51 | + key_prefix: |
| 52 | + description: 'Cache key substring to match (delete-entries mode only; scrub-ivydata always operates on the latest cache per OS)' |
| 53 | + required: false |
| 54 | + default: 'grape-' |
| 55 | + dry_run: |
| 56 | + description: 'List/preview without modifying' |
| 57 | + required: false |
| 58 | + type: boolean |
| 59 | + default: false |
| 60 | + |
| 61 | +permissions: |
| 62 | + # `actions: write` is required both for `gh cache delete` and for the |
| 63 | + # `actions/cache` post-step that saves a fresh entry in scrub mode. |
| 64 | + actions: write |
| 65 | + contents: read |
| 66 | + |
| 67 | +jobs: |
| 68 | + delete-entries: |
| 69 | + if: ${{ inputs.mode == 'delete-entries' }} |
| 70 | + runs-on: ubuntu-latest |
| 71 | + steps: |
| 72 | + - name: "🔍 List matching cache entries" |
| 73 | + env: |
| 74 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 75 | + GH_REPO: ${{ github.repository }} |
| 76 | + PREFIX: ${{ inputs.key_prefix }} |
| 77 | + run: | |
| 78 | + echo "Caches whose keys contain '${PREFIX}':" |
| 79 | + gh cache list --limit 100 \ |
| 80 | + --json id,key,sizeInBytes,createdAt \ |
| 81 | + --jq ".[] | select(.key | contains(\"${PREFIX}\"))" \ |
| 82 | + | tee matches.json |
| 83 | + echo |
| 84 | + echo "Total matched: $(jq -s 'length' matches.json)" |
| 85 | + echo "Total size: $(jq -s 'map(.sizeInBytes) | add // 0' matches.json) bytes" |
| 86 | + - name: "🗑️ Delete matching cache entries" |
| 87 | + if: ${{ inputs.dry_run == false }} |
| 88 | + env: |
| 89 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 90 | + GH_REPO: ${{ github.repository }} |
| 91 | + PREFIX: ${{ inputs.key_prefix }} |
| 92 | + run: | |
| 93 | + # `gh cache list` paginates by default; --limit 100 plus the |
| 94 | + # loop below handles repos with many entries (purge tends to |
| 95 | + # be one-shot anyway). |
| 96 | + while true; do |
| 97 | + ids=$(gh cache list --limit 100 \ |
| 98 | + --json id,key \ |
| 99 | + --jq ".[] | select(.key | contains(\"${PREFIX}\")) | .id") |
| 100 | + if [ -z "$ids" ]; then |
| 101 | + echo "No more matching caches." |
| 102 | + break |
| 103 | + fi |
| 104 | + echo "$ids" | while read -r id; do |
| 105 | + [ -z "$id" ] && continue |
| 106 | + echo "Deleting cache id=$id" |
| 107 | + gh cache delete "$id" |
| 108 | + done |
| 109 | + done |
| 110 | +
|
| 111 | + # Surgical scrub: per-OS, restore the most recent grape cache for that |
| 112 | + # OS, delete only the `ivydata-*.properties` negative-cache markers, |
| 113 | + # then let actions/cache's post-step save the cleaned tree under a |
| 114 | + # fresh run-id-keyed entry. All JARs/POMs survive. |
| 115 | + # |
| 116 | + # The matrix mirrors the OSes that produce grape caches in the build |
| 117 | + # workflows (Linux/Windows/macOS). Each scrubs its own key family. |
| 118 | + scrub-ivydata: |
| 119 | + if: ${{ inputs.mode == 'scrub-ivydata' }} |
| 120 | + strategy: |
| 121 | + fail-fast: false |
| 122 | + matrix: |
| 123 | + os: [ubuntu-latest, windows-latest, macos-latest] |
| 124 | + runs-on: ${{ matrix.os }} |
| 125 | + steps: |
| 126 | + - name: "🍇 Restore latest grape cache for ${{ runner.os }}" |
| 127 | + uses: actions/cache@v4 |
| 128 | + with: |
| 129 | + path: ~/.groovy/grapes |
| 130 | + # New entry key per run so the cleaned tree gets saved under a |
| 131 | + # fresh tag (actions/cache won't overwrite an existing key). |
| 132 | + # Restore-keys finds the most recent `<os>-grape-*` entry. |
| 133 | + key: ${{ runner.os }}-grape-${{ github.run_id }} |
| 134 | + restore-keys: | |
| 135 | + ${{ runner.os }}-grape- |
| 136 | + - name: "🔍 Inventory poisoned ivydata-*.properties markers" |
| 137 | + shell: bash |
| 138 | + run: | |
| 139 | + if [ ! -d ~/.groovy/grapes ]; then |
| 140 | + echo "No grape cache restored on ${{ runner.os }} — nothing to do." |
| 141 | + exit 0 |
| 142 | + fi |
| 143 | + echo "ivydata-*.properties files in ~/.groovy/grapes:" |
| 144 | + # Print each file with size and mtime, plus a count. |
| 145 | + # `find ... -printf` isn't portable to macOS, so use stat. |
| 146 | + count=0 |
| 147 | + while IFS= read -r f; do |
| 148 | + count=$((count + 1)) |
| 149 | + ls -l "$f" |
| 150 | + done < <(find ~/.groovy/grapes -name 'ivydata-*.properties' 2>/dev/null) |
| 151 | + echo |
| 152 | + echo "Total markers: $count" |
| 153 | + - name: "🧹 Scrub ivydata-*.properties markers" |
| 154 | + if: ${{ inputs.dry_run == false }} |
| 155 | + shell: bash |
| 156 | + run: | |
| 157 | + if [ ! -d ~/.groovy/grapes ]; then |
| 158 | + echo "No grape cache restored — skipping." |
| 159 | + exit 0 |
| 160 | + fi |
| 161 | + deleted=$(find ~/.groovy/grapes -name 'ivydata-*.properties' -print -delete | wc -l) |
| 162 | + echo "Deleted $deleted ivydata-*.properties marker(s)." |
| 163 | + # Sanity-check that JARs survived. A non-zero count here is |
| 164 | + # the whole point of scrub mode vs. delete-entries mode. |
| 165 | + jars=$(find ~/.groovy/grapes -name '*.jar' 2>/dev/null | wc -l) |
| 166 | + echo "JARs remaining in cache: $jars" |
0 commit comments