Skip to content

Commit dea71cc

Browse files
committed
github actions for grape caching
1 parent 8bdcbb9 commit dea71cc

3 files changed

Lines changed: 209 additions & 0 deletions

File tree

.github/workflows/groovy-build-coverage.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ jobs:
3737
distribution: 'zulu'
3838
java-version: 21
3939
check-latest: true
40+
# `setup-gradle` caches ~/.gradle/caches but NOT ~/.groovy/grapes,
41+
# which is where @Grab-resolved artifacts (used by tests like
42+
# GenericsSTCTest, MethodReferenceTest, …) land. Caching it makes
43+
# the build resilient to transient Maven Central / CDN outages:
44+
# once an artifact has been resolved on any prior run, subsequent
45+
# runs reuse it from cache and don't re-hit the network.
46+
#
47+
# Same key prefix as `groovy-build-test.yml` so the two workflows
48+
# share their accumulated Grape cache.
49+
- name: "🍇 Cache @Grab artifacts (~/.groovy/grapes)"
50+
uses: actions/cache@v4
51+
with:
52+
path: ~/.groovy/grapes
53+
key: ${{ runner.os }}-grape-${{ github.run_id }}
54+
restore-keys: |
55+
${{ runner.os }}-grape-
4056
- uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
4157
- name: Test with Gradle
4258
run: ./gradlew -Pcoverage=true jacocoAllReport

.github/workflows/groovy-build-test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@ jobs:
5353
${{ matrix.java }}
5454
21
5555
check-latest: true
56+
# `setup-gradle` below caches ~/.gradle/caches but NOT ~/.groovy/grapes,
57+
# which is where @Grab-resolved artifacts (used by tests like
58+
# GenericsSTCTest, MethodReferenceTest, …) land. Caching it makes the
59+
# build resilient to transient Maven Central / CDN outages: once an
60+
# artifact has been resolved on any prior run, subsequent runs reuse
61+
# it from cache and don't re-hit the network.
62+
#
63+
# Key strategy: per-run key + prefix restore-keys. Each run saves a
64+
# fresh entry; the next run finds the most recent via prefix
65+
# fallback. The cache grows with new @Grab coordinates over time and
66+
# never gets invalidated by older ones being removed.
67+
- name: "🍇 Cache @Grab artifacts (~/.groovy/grapes)"
68+
uses: actions/cache@v4
69+
with:
70+
path: ~/.groovy/grapes
71+
key: ${{ runner.os }}-grape-${{ github.run_id }}
72+
restore-keys: |
73+
${{ runner.os }}-grape-
5674
- name: "🐘 Setup Gradle"
5775
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
5876
- name: "🔍 Setup TestLens"
@@ -85,6 +103,15 @@ jobs:
85103
${{ matrix.java }}
86104
21
87105
check-latest: true
106+
# See the lts job for rationale; same prefix lets both jobs share
107+
# the cache.
108+
- name: "🍇 Cache @Grab artifacts (~/.groovy/grapes)"
109+
uses: actions/cache@v4
110+
with:
111+
path: ~/.groovy/grapes
112+
key: ${{ runner.os }}-grape-${{ github.run_id }}
113+
restore-keys: |
114+
${{ runner.os }}-grape-
88115
- uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
89116
- name: "🏃Test with Gradle"
90117
run: ./gradlew test -Ptarget.java.home="$JAVA_HOME_${{ matrix.java }}_X64"
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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

Comments
 (0)