From a6e5233773035741f903542b4478400560f36e86 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Wed, 5 Aug 2026 09:51:33 +0200 Subject: [PATCH 1/9] [MINOR] Persist the Maven build cache between CI runs .mvn/extensions.xml enables maven-build-cache-extension, but the cache never survives a CI run: actions/setup-java's `cache: maven` persists ~/.m2/repository only, while the extension writes to ~/.m2/build-cache. The effect is visible in any recent build log - ten modules, ten "Local build was not found by checksum" lines, zero restores - so every run rebuilds and retests all ten modules from scratch, including for pull requests that touch only the website or a single module. Restore ~/.m2/build-cache before the build and save it afterwards. Only main writes an entry; pull requests restore from main's rather than each branch consuming the repository-wide 10 GB cache budget. Source Build Check is deliberately left alone - it passes -Dmaven.build.cache.enabled=false and skips `cache: maven` on purpose, to prove the source release builds without prebuilt artifacts. --- .github/workflows/mvn-ci-build.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/mvn-ci-build.yml b/.github/workflows/mvn-ci-build.yml index 92e478940..eb5ae7bcb 100644 --- a/.github/workflows/mvn-ci-build.yml +++ b/.github/workflows/mvn-ci-build.yml @@ -43,5 +43,25 @@ jobs: distribution: 'temurin' cache: maven + # `cache: maven` above only persists ~/.m2/repository. The build cache + # extension configured in .mvn/extensions.xml writes to ~/.m2/build-cache, + # so without this it starts empty on every run and never restores a module. + # Only `main` writes an entry, so pull requests warm up from `main` instead + # of each branch filling the repository-wide cache budget. + - name: Restore Maven build cache + uses: actions/cache/restore@v6 + with: + path: ~/.m2/build-cache + key: maven-build-cache-${{ runner.os }}-${{ github.sha }} + restore-keys: | + maven-build-cache-${{ runner.os }}- + - name: Build all module with Maven run: ./mvnw clean install -ntp -B + + - name: Save Maven build cache + if: github.ref == 'refs/heads/main' && success() + uses: actions/cache/save@v6 + with: + path: ~/.m2/build-cache + key: maven-build-cache-${{ runner.os }}-${{ github.sha }} From e9bf7b4dd6c1dd7a847a4c542e62bbc92d4b234f Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 00:29:05 +0200 Subject: [PATCH 2/9] Keep the bundled jars out of the persisted build cache Persisting ~/.m2/build-cache as the previous commit does is only useful if the entry is small enough to live alongside the ~/.m2/repository caches in the repository-wide 10 GB budget. Measured, it is not: one generation is 1.2 GB, and 1.17 GB of that is three bundled jars - xtable-utilities (1.0 GB), xtable-hive-metastore (125 MB) and xtable-aws (42 MB). Those three modules are worth about 3 minutes of a 27 minute build. xtable-core and xtable-service are 84% of the time and about 1 MB of cache between them. Caching is worth it in inverse proportion to artifact size here, so opt the bundle producers out with the extension's per-project property, which disables both restore and save for that module. A cold build then writes 12 MB instead of 1.2 GB while still caching everything expensive; measured locally, a fully warm run restores 7 of 7 remaining modules. Also in this commit: - Add .mvn/maven-build-cache-config.xml pinning maxBuildsCached to 1, disabling the remote cache explicitly, and reconciling the surefire skip flags. Command line flags are not part of a module's checksum, so without reconciliation a build that skipped tests and one that ran them are indistinguishable to the cache. The flags are tracked without a skipValue attribute deliberately: skipValue relaxes the comparison rather than tightening it. - Bump maven-build-cache-extension 1.1.0 -> 1.2.3. Two invalidation fixes land in 1.2.1: MBUILDCACHE-87, plugin dependencies missing from the checksum, and MBUILDCACHE-99, moved files not always detected. Both affect whether a restore is correct, so they matter more once the cache actually persists. - Guard the save step on the cache directory staying under 100 MB, and warn when it does not, so a future module that starts caching large artifacts is visible rather than silently churning the budget. - Pass -Dmaven.build.cache.enabled=false when publishing a release. versions:set rewrites the version immediately beforehand, while cache restoration is version-agnostic. That runner keeps no cache directory today, so this is a guard against it gaining one, and it matches what source-build-check.yml already does deliberately. - Add the JDK to the Actions cache key, since the extension does not hash the JDK, and raise the job timeout from 30 to 40 minutes - a cold build is already 27 minutes against it, independent of caching. - Teach the spotless license-header delimiter about false in their + # own poms, which keeps this entry around 15 MB. Without that opt-out the + # entry is 1.2 GB, 97% of it three shaded jars that account for only ~3 of + # the 27 minutes a cold build takes. + # + # Note: a cache-restored xtable-service does not reproduce + # target/quarkus-app. No step consumes that directory today; anything added + # later that does must not rely on it being present. - name: Restore Maven build cache uses: actions/cache/restore@v6 with: path: ~/.m2/build-cache - key: maven-build-cache-${{ runner.os }}-${{ github.sha }} + key: maven-build-cache-${{ runner.os }}-jdk11-${{ github.sha }} restore-keys: | - maven-build-cache-${{ runner.os }}- + maven-build-cache-${{ runner.os }}-jdk11- - name: Build all module with Maven run: ./mvnw clean install -ntp -B + # Guard against a module starting to cache large artifacts again: warn and + # skip the save rather than churn the repository-wide 10 GB cache budget, + # which is otherwise mostly the ~/.m2/repository entries above. Checking the + # size rather than specific paths keeps this working across module renames. + - name: Check Maven build cache size + id: build-cache-size + if: success() && github.ref == 'refs/heads/main' + run: | + size_mb=$(du -sm ~/.m2/build-cache 2>/dev/null | cut -f1) + size_mb=${size_mb:-0} + echo "size_mb=${size_mb}" >> "$GITHUB_OUTPUT" + echo "~/.m2/build-cache is ${size_mb} MB" + if [ "${size_mb}" -gt 100 ]; then + echo "::warning title=Maven build cache not saved::~/.m2/build-cache is ${size_mb} MB (limit 100 MB). A module is caching large artifacts; opt it out with false in its pom." + fi + - name: Save Maven build cache - if: github.ref == 'refs/heads/main' && success() + if: success() && github.ref == 'refs/heads/main' && steps.build-cache-size.outputs.size_mb < 100 uses: actions/cache/save@v6 with: path: ~/.m2/build-cache - key: maven-build-cache-${{ runner.os }}-${{ github.sha }} + key: maven-build-cache-${{ runner.os }}-jdk11-${{ github.sha }} diff --git a/.github/workflows/package-deploy.yml b/.github/workflows/package-deploy.yml index 7083a8f77..078d0455e 100644 --- a/.github/workflows/package-deploy.yml +++ b/.github/workflows/package-deploy.yml @@ -35,6 +35,12 @@ jobs: - name: Set Version run: ./mvnw versions:set -DnewVersion="$VERSION" - name: Publish package - run: ./mvnw -ntp --batch-mode deploy -DskipTests + # Never build a release from cached module output. `versions:set` above has + # just rewritten the version, while cache restoration is version-agnostic — + # it stamps the current project version onto whatever it restores. This + # runner keeps no build-cache directory today, so the flag is a guard + # against that changing, and it also saves checksumming every module for a + # cache that can never hit. Same reasoning as source-build-check.yml. + run: ./mvnw -ntp --batch-mode deploy -DskipTests -Dmaven.build.cache.enabled=false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 1fb0710e0..2d3d23d20 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -20,6 +20,6 @@ org.apache.maven.extensions maven-build-cache-extension - 1.1.0 + 1.2.3 diff --git a/.mvn/maven-build-cache-config.xml b/.mvn/maven-build-cache-config.xml new file mode 100644 index 000000000..9a96a55b4 --- /dev/null +++ b/.mvn/maven-build-cache-config.xml @@ -0,0 +1,59 @@ + + + + + true + + + + 1 + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index f7ecf7b61..da1a517c5 100644 --- a/pom.xml +++ b/pom.xml @@ -953,7 +953,8 @@ style/xml-license-header - ^<project|^<configuration|^<Configuration|^<extensions|^<component + + ^<project|^<configuration|^<Configuration|^<extensions|^<component|^<cache ^<.*xml.+?$ diff --git a/xtable-aws/pom.xml b/xtable-aws/pom.xml index 3d0a6639e..9652042df 100644 --- a/xtable-aws/pom.xml +++ b/xtable-aws/pom.xml @@ -27,6 +27,17 @@ xtable-aws XTable AWS + + + false + diff --git a/xtable-hive-metastore/pom.xml b/xtable-hive-metastore/pom.xml index 36071bfec..e6af6d15c 100644 --- a/xtable-hive-metastore/pom.xml +++ b/xtable-hive-metastore/pom.xml @@ -27,6 +27,17 @@ xtable-hive-metastore XTable HMS + + + false + diff --git a/xtable-utilities/pom.xml b/xtable-utilities/pom.xml index 53ad1497d..fd706ebfa 100644 --- a/xtable-utilities/pom.xml +++ b/xtable-utilities/pom.xml @@ -27,6 +27,17 @@ xtable-utilities_${scala.binary.version} XTable Project Utilities + + + false + From fd12a7b05a3572c936537d83ddc0cd1137337daa Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 01:30:50 +0200 Subject: [PATCH 3/9] Drop this project's own artifacts before the m2 repository cache is saved `mvn install` puts xtable's own artifacts into ~/.m2/repository, and that includes the bundled jars - about 1.2 GB, of which xtable-utilities alone is roughly 1 GB. actions/setup-java caches that directory wholesale and offers no way to exclude a path, so remove them in a final step, which runs before its post-save hook. They are rebuilt from source on every run, so they can never produce a useful cache hit, and a stale SNAPSHOT sitting in the local repository can shadow a reactor build. This is a guard rather than a fix. License Check currently reserves the shared cache key first - it downloads the whole dependency tree to inspect each jar's license, which is the useful half - and because it only runs `package`, the saved entry has never contained this project's artifacts. That is why this job logs "Unable to reserve cache with key" at the end of every run. The arrangement is the one we want, but it holds by ordering rather than by construction: if License Check ever fails or is cancelled before its post step, this job would win the key and save roughly 2.15 GB into a repository-wide 10 GB budget. --- .github/workflows/mvn-ci-build.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/mvn-ci-build.yml b/.github/workflows/mvn-ci-build.yml index 534461cb1..86b44498d 100644 --- a/.github/workflows/mvn-ci-build.yml +++ b/.github/workflows/mvn-ci-build.yml @@ -92,3 +92,17 @@ jobs: with: path: ~/.m2/build-cache key: maven-build-cache-${{ runner.os }}-jdk11-${{ github.sha }} + + # `install` above puts this project's own artifacts into ~/.m2/repository, + # including the bundled jars (~1.2 GB, xtable-utilities alone is ~1 GB). + # actions/setup-java caches that whole directory with no way to exclude a + # path, so drop them before its post step runs. They are rebuilt from + # source on every run and can never produce a useful cache hit, and a + # stale SNAPSHOT left in the local repository can shadow a reactor build. + # + # In practice License Check wins the cache key first and it only runs + # `package`, so today's entry already excludes these. This keeps that true + # if this job ever becomes the one that saves. + - name: Drop this project's own artifacts from the Maven repository cache + if: always() + run: rm -rf ~/.m2/repository/org/apache/xtable From 4972ec8dc98f803a74d49a37f8ec4701336ca75f Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 01:47:17 +0200 Subject: [PATCH 4/9] Manage the Maven repository cache explicitly, with one writer Both jobs used setup-java's `cache: maven`, which has two problems here. It restores on an exact key match only. setup-java sets no restore-keys deliberately, "to start with a clear cache after dependency update" (actions/setup-java#269), so any change to a pom or to extensions.xml made every job re-download the entire dependency tree from Central. Keying the same way but adding a prefix fallback warms from the previous entry and downloads only the delta. This commit changes both files, so it pays that cost once itself. It also gave the two jobs the same key, and cache entries are immutable, so whichever finished first reserved it and the other's save failed. License Check finishes in about two minutes and the full build takes 27, so in practice License Check always won and Maven CI Build logged "Unable to reserve cache with key" on every run. That outcome was the right one, which is why this makes it explicit rather than trying to win the race. License Check has to resolve every dependency in order to inspect each jar's license, so it populates exactly what the other jobs need, and it stops at `package`, so its entry never contains this project's own artifacts. It is now the sole writer; Maven CI Build restores the same key read-only and no longer attempts a save. The wrapper distribution was cached separately by setup-java, keyed on the wrapper properties so it survives pom changes. That is reproduced here rather than lost. Both jobs also clear ~/.m2/repository/org/apache/xtable before the post-job save, as Source Build Check already does. On the writer that guards the invariant above; on the reader it is belt and braces. --- .github/workflows/mvn-ci-build.yml | 38 +++++++++++++++++++------ .github/workflows/mvn-license-check.yml | 38 ++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/.github/workflows/mvn-ci-build.yml b/.github/workflows/mvn-ci-build.yml index 86b44498d..e33cf22d5 100644 --- a/.github/workflows/mvn-ci-build.yml +++ b/.github/workflows/mvn-ci-build.yml @@ -42,11 +42,32 @@ jobs: with: java-version: '11' distribution: 'temurin' - cache: maven - # `cache: maven` above only persists ~/.m2/repository. The build cache - # extension configured in .mvn/extensions.xml writes to ~/.m2/build-cache, - # so without this it starts empty on every run and never restores a module. + # Restore only. License Check writes this cache: it resolves every + # dependency to inspect each jar's license, so it populates exactly what + # this job needs, it finishes in a couple of minutes, and it stops at + # `package` so the entry never carries this project's own artifacts. + # Previously both jobs used setup-java's `cache: maven`, which gave them + # the same key, and whichever finished first reserved it - this job then + # logged "Unable to reserve cache with key" on every single run. Making + # the reader explicit removes the race rather than winning it. + - name: Restore Maven repository cache + uses: actions/cache/restore@v6 + with: + path: ~/.m2/repository + key: m2-repository-${{ runner.os }}-jdk11-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} + restore-keys: | + m2-repository-${{ runner.os }}-jdk11- + + - name: Restore Maven wrapper cache + uses: actions/cache/restore@v6 + with: + path: ~/.m2/wrapper/dists + key: m2-wrapper-${{ runner.os }}-${{ hashFiles('**/.mvn/wrapper/maven-wrapper.properties') }} + + # A separate cache from the repository above: the build cache extension + # configured in .mvn/extensions.xml writes to ~/.m2/build-cache, so + # without this it starts empty every run and never restores a module. # Only `main` writes an entry, so pull requests warm up from `main` instead # of each branch filling the repository-wide cache budget. # @@ -99,10 +120,11 @@ jobs: # path, so drop them before its post step runs. They are rebuilt from # source on every run and can never produce a useful cache hit, and a # stale SNAPSHOT left in the local repository can shadow a reactor build. + # Source Build Check clears the same path for the same reason. # - # In practice License Check wins the cache key first and it only runs - # `package`, so today's entry already excludes these. This keeps that true - # if this job ever becomes the one that saves. - - name: Drop this project's own artifacts from the Maven repository cache + # This job no longer saves that cache, so nothing depends on this today. + # It is kept so the invariant holds by construction rather than by which + # step happens to write last. + - name: Drop this project's own artifacts from the Maven repository if: always() run: rm -rf ~/.m2/repository/org/apache/xtable diff --git a/.github/workflows/mvn-license-check.yml b/.github/workflows/mvn-license-check.yml index 889c363ba..84df39c3d 100644 --- a/.github/workflows/mvn-license-check.yml +++ b/.github/workflows/mvn-license-check.yml @@ -40,7 +40,34 @@ jobs: with: java-version: '11' distribution: 'temurin' - cache: maven + + # ~/.m2/repository is cached here rather than via setup-java's + # `cache: maven`, which restores on an exact key match only - it sets no + # restore-keys by design (actions/setup-java#269), so any pom change made + # every job re-download the whole dependency tree from Central. The prefix + # fallback below warms from the previous entry and downloads just the delta. + # + # This job is the writer for that cache, and deliberately the only one. + # It has to resolve every dependency in order to inspect each jar's + # license, so it populates exactly what the other jobs need, and it stops + # at `package`, so this project's own artifacts never enter the entry. + # Maven CI Build restores the same key read-only. + - name: Cache Maven repository + uses: actions/cache@v6 + with: + path: ~/.m2/repository + key: m2-repository-${{ runner.os }}-jdk11-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} + restore-keys: | + m2-repository-${{ runner.os }}-jdk11- + + # Replaces the separate wrapper cache that setup-java's `cache: maven` + # used to provide. Keyed only on the wrapper properties, which change + # rarely, so it survives the pom changes that rotate the key above. + - name: Cache Maven wrapper + uses: actions/cache@v6 + with: + path: ~/.m2/wrapper/dists + key: m2-wrapper-${{ runner.os }}-${{ hashFiles('**/.mvn/wrapper/maven-wrapper.properties') }} - name: Apache License Check run: ./mvnw apache-rat:check -B @@ -61,3 +88,12 @@ jobs: - name: Validate Bundled License Texts run: python3 release/scripts/validate_bundled_license_texts.py + + # This job writes the repository cache, so the invariant that the entry + # holds third-party dependencies only is enforced here. The steps above + # stop at `package`, so this is a no-op today; it exists so that adding + # an `install` later cannot silently add ~1.2 GB of bundled jars to the + # entry. Runs before actions/cache's post-job save. + - name: Drop this project's own artifacts from the Maven repository + if: always() + run: rm -rf ~/.m2/repository/org/apache/xtable From a289782efe01923fb859d4298a52de29747ff334 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 01:56:09 +0200 Subject: [PATCH 5/9] Write the Maven repository cache only from main The entry measures 981 MB against a repository-wide 10 GB budget, so a handful of concurrent pull requests with differing pom hashes would crowd out the entry they all restore from. Split restore and save so only main writes, matching what this branch already does for the build cache. Pull requests warm from main through the prefix fallback and re-download only their own delta. Saving is also conditional on the exact key having missed. Cache entries are immutable, so writing over an existing key just fails; that check is what actions/cache performs internally when restore and save are not split. The wrapper cache is left unsplit. At ~9 MB it is not worth gating, and it is keyed on the wrapper properties, which rarely change. --- .github/workflows/mvn-ci-build.yml | 8 ++++---- .github/workflows/mvn-license-check.yml | 27 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mvn-ci-build.yml b/.github/workflows/mvn-ci-build.yml index e33cf22d5..34b86108b 100644 --- a/.github/workflows/mvn-ci-build.yml +++ b/.github/workflows/mvn-ci-build.yml @@ -43,10 +43,10 @@ jobs: java-version: '11' distribution: 'temurin' - # Restore only. License Check writes this cache: it resolves every - # dependency to inspect each jar's license, so it populates exactly what - # this job needs, it finishes in a couple of minutes, and it stops at - # `package` so the entry never carries this project's own artifacts. + # Restore only. License Check writes this cache, and only on `main`: it + # resolves every dependency to inspect each jar's license, so it populates + # exactly what this job needs, it finishes in a couple of minutes, and it + # stops at `package` so the entry never carries this project's artifacts. # Previously both jobs used setup-java's `cache: maven`, which gave them # the same key, and whichever finished first reserved it - this job then # logged "Unable to reserve cache with key" on every single run. Making diff --git a/.github/workflows/mvn-license-check.yml b/.github/workflows/mvn-license-check.yml index 84df39c3d..9cb15d4fc 100644 --- a/.github/workflows/mvn-license-check.yml +++ b/.github/workflows/mvn-license-check.yml @@ -52,8 +52,14 @@ jobs: # license, so it populates exactly what the other jobs need, and it stops # at `package`, so this project's own artifacts never enter the entry. # Maven CI Build restores the same key read-only. - - name: Cache Maven repository - uses: actions/cache@v6 + # + # Restore and save are split so that only `main` writes. The entry is + # ~1 GB against a repository-wide 10 GB budget, so letting every branch + # write one would crowd out the entry they all restore from. Pull requests + # warm from `main` via the prefix and re-download only their own delta. + - name: Restore Maven repository cache + id: m2-repository + uses: actions/cache/restore@v6 with: path: ~/.m2/repository key: m2-repository-${{ runner.os }}-jdk11-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} @@ -62,7 +68,8 @@ jobs: # Replaces the separate wrapper cache that setup-java's `cache: maven` # used to provide. Keyed only on the wrapper properties, which change - # rarely, so it survives the pom changes that rotate the key above. + # rarely, so it survives the pom changes that rotate the key above. Left + # unsplit unlike the repository cache: at ~9 MB it is not worth gating. - name: Cache Maven wrapper uses: actions/cache@v6 with: @@ -93,7 +100,19 @@ jobs: # holds third-party dependencies only is enforced here. The steps above # stop at `package`, so this is a no-op today; it exists so that adding # an `install` later cannot silently add ~1.2 GB of bundled jars to the - # entry. Runs before actions/cache's post-job save. + # entry. Must stay ahead of the save below. - name: Drop this project's own artifacts from the Maven repository if: always() run: rm -rf ~/.m2/repository/org/apache/xtable + + # Only when the exact key missed - entries are immutable, so saving over + # an existing key just fails. This is what actions/cache does internally + # when restore and save are not split. + - name: Save Maven repository cache + if: > + success() && github.ref == 'refs/heads/main' + && steps.m2-repository.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: ~/.m2/repository + key: m2-repository-${{ runner.os }}-jdk11-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} From 3414da6094e59f60f490deda527246b3226218e9 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 23:13:45 +0200 Subject: [PATCH 6/9] Reconcile the flags that skip tests without changing the checksum Failsafe is bound in the parent POM to integration-test and verify, and CI runs `install`, so integration tests run on every build and a restored module skips them exactly as it skips unit tests. Reconciles are matched per goal, and the two goals expose different parameters: testFailureIgnore is on verify only, and naming a parameter a goal does not have earns a warning on every build. The compiler entry closes a hole the surefire block does not reach. -Dmaven.test.skip=true never gets to surefire here, because the parent POM pins ${skipUTs} and explicit plugin configuration wins over the user property, so surefire's skip parameter reads identically either way and the module checksum is byte-identical across the two. What the flag actually suppresses is test compilation. Verified on xtable-api, Temurin 11: seeding the cache with -Dmaven.test.skip=true and then running a plain `clean install` restored the untested build before this commit, and after it logs "Plugin parameter mismatch found. Parameter: skip, expected: true, actual: false" and runs the tests. A plain build still restores a plain build, and buildinfo.xml records all nine failsafe parameters as tracked="true" with no "Cannot find a Mojo parameter" warnings. --- .mvn/maven-build-cache-config.xml | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/.mvn/maven-build-cache-config.xml b/.mvn/maven-build-cache-config.xml index 9a96a55b4..172161ff7 100644 --- a/.mvn/maven-build-cache-config.xml +++ b/.mvn/maven-build-cache-config.xml @@ -53,6 +53,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 16035f4ff38a4ce8916cabdd4a5d2cc79539f7ac Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 23:14:42 +0200 Subject: [PATCH 7/9] Derive the JDK in the cache keys from one place The JDK is in these keys because the build cache extension does not hash it, and the two workflows have to agree on the key or the reader stops finding what the writer saved. Four literal "jdk11" strings across two files could not express that; bumping actions/setup-java's java-version would have left them asserting a JDK that is no longer in use. --- .github/workflows/mvn-ci-build.yml | 20 +++++++++++++------- .github/workflows/mvn-license-check.yml | 15 ++++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/mvn-ci-build.yml b/.github/workflows/mvn-ci-build.yml index 34b86108b..a718ba824 100644 --- a/.github/workflows/mvn-ci-build.yml +++ b/.github/workflows/mvn-ci-build.yml @@ -29,6 +29,12 @@ on: branches: - "main" +# The build cache extension does not hash the JDK, so it is part of every cache +# key below. Defined once here rather than repeated, so that changing the JDK +# cannot leave a key asserting the old one. +env: + JDK_VERSION: '11' + jobs: build: runs-on: ubuntu-latest @@ -37,10 +43,10 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Set up JDK 11 + - name: Set up JDK ${{ env.JDK_VERSION }} uses: actions/setup-java@v5 with: - java-version: '11' + java-version: ${{ env.JDK_VERSION }} distribution: 'temurin' # Restore only. License Check writes this cache, and only on `main`: it @@ -55,9 +61,9 @@ jobs: uses: actions/cache/restore@v6 with: path: ~/.m2/repository - key: m2-repository-${{ runner.os }}-jdk11-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} + key: m2-repository-${{ runner.os }}-jdk${{ env.JDK_VERSION }}-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} restore-keys: | - m2-repository-${{ runner.os }}-jdk11- + m2-repository-${{ runner.os }}-jdk${{ env.JDK_VERSION }}- - name: Restore Maven wrapper cache uses: actions/cache/restore@v6 @@ -84,9 +90,9 @@ jobs: uses: actions/cache/restore@v6 with: path: ~/.m2/build-cache - key: maven-build-cache-${{ runner.os }}-jdk11-${{ github.sha }} + key: maven-build-cache-${{ runner.os }}-jdk${{ env.JDK_VERSION }}-${{ github.sha }} restore-keys: | - maven-build-cache-${{ runner.os }}-jdk11- + maven-build-cache-${{ runner.os }}-jdk${{ env.JDK_VERSION }}- - name: Build all module with Maven run: ./mvnw clean install -ntp -B @@ -112,7 +118,7 @@ jobs: uses: actions/cache/save@v6 with: path: ~/.m2/build-cache - key: maven-build-cache-${{ runner.os }}-jdk11-${{ github.sha }} + key: maven-build-cache-${{ runner.os }}-jdk${{ env.JDK_VERSION }}-${{ github.sha }} # `install` above puts this project's own artifacts into ~/.m2/repository, # including the bundled jars (~1.2 GB, xtable-utilities alone is ~1 GB). diff --git a/.github/workflows/mvn-license-check.yml b/.github/workflows/mvn-license-check.yml index 9cb15d4fc..582ced6d2 100644 --- a/.github/workflows/mvn-license-check.yml +++ b/.github/workflows/mvn-license-check.yml @@ -29,16 +29,21 @@ on: branches: - "main" +# Kept in step with mvn-ci-build.yml: this job writes the cache that job reads, +# and the JDK is part of the key on both sides. +env: + JDK_VERSION: '11' + jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Set up JDK 11 + - name: Set up JDK ${{ env.JDK_VERSION }} uses: actions/setup-java@v5 with: - java-version: '11' + java-version: ${{ env.JDK_VERSION }} distribution: 'temurin' # ~/.m2/repository is cached here rather than via setup-java's @@ -62,9 +67,9 @@ jobs: uses: actions/cache/restore@v6 with: path: ~/.m2/repository - key: m2-repository-${{ runner.os }}-jdk11-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} + key: m2-repository-${{ runner.os }}-jdk${{ env.JDK_VERSION }}-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} restore-keys: | - m2-repository-${{ runner.os }}-jdk11- + m2-repository-${{ runner.os }}-jdk${{ env.JDK_VERSION }}- # Replaces the separate wrapper cache that setup-java's `cache: maven` # used to provide. Keyed only on the wrapper properties, which change @@ -115,4 +120,4 @@ jobs: uses: actions/cache/save@v6 with: path: ~/.m2/repository - key: m2-repository-${{ runner.os }}-jdk11-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} + key: m2-repository-${{ runner.os }}-jdk${{ env.JDK_VERSION }}-${{ hashFiles('**/pom.xml', '**/.mvn/extensions.xml') }} From 38ffe625c6e2e00e037833e4addd4f8f0a48bb56 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 23:15:04 +0200 Subject: [PATCH 8/9] Resolve the whole reactor in the job that writes the repository cache "License Check has to resolve every dependency in order to inspect each jar's license" is not true, and the previous commit made it load- bearing. apache-rat:check declares no dependency resolution; dependency:tree collects poms rather than jars; and the shaded-bundle step names three modules and reaches only their -am closure. -am makes dependencies, not dependents, so the two leaves are outside it: nothing in the job resolves xtable-service's Quarkus stack, xtable-utilities' bundled dependencies, or maven-install-plugin, which only an `install` pulls. Maven CI Build is restore-only by construction, so it cannot fill that gap itself - it would re-download the delta from Central on every run, permanently. dependency:go-offline over the full reactor covers what the license steps do not. It runs only on main, where the save happens, and its failure must not fail a license check: an incomplete entry costs download time on the next run and nothing else. Verified: `./mvnw -B -ntp -q dependency:go-offline -Dmaven.build.cache.enabled=false` exits 0 across the reactor. --- .github/workflows/mvn-ci-build.yml | 10 +++++--- .github/workflows/mvn-license-check.yml | 33 +++++++++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mvn-ci-build.yml b/.github/workflows/mvn-ci-build.yml index a718ba824..ef32cf4e0 100644 --- a/.github/workflows/mvn-ci-build.yml +++ b/.github/workflows/mvn-ci-build.yml @@ -50,13 +50,17 @@ jobs: distribution: 'temurin' # Restore only. License Check writes this cache, and only on `main`: it - # resolves every dependency to inspect each jar's license, so it populates - # exactly what this job needs, it finishes in a couple of minutes, and it - # stops at `package` so the entry never carries this project's artifacts. + # finishes in a couple of minutes against this job's 27, and it stops at + # `package` so the entry never carries this project's artifacts. It + # resolves the reactor explicitly for this job's benefit; its own license + # steps reach only part of it. # Previously both jobs used setup-java's `cache: maven`, which gave them # the same key, and whichever finished first reserved it - this job then # logged "Unable to reserve cache with key" on every single run. Making # the reader explicit removes the race rather than winning it. + # + # A warm start, not a guarantee: `install` here runs plugins that a build + # stopping at `package` never resolves, so expect a delta every run. - name: Restore Maven repository cache uses: actions/cache/restore@v6 with: diff --git a/.github/workflows/mvn-license-check.yml b/.github/workflows/mvn-license-check.yml index 582ced6d2..ed6186933 100644 --- a/.github/workflows/mvn-license-check.yml +++ b/.github/workflows/mvn-license-check.yml @@ -52,11 +52,16 @@ jobs: # every job re-download the whole dependency tree from Central. The prefix # fallback below warms from the previous entry and downloads just the delta. # - # This job is the writer for that cache, and deliberately the only one. - # It has to resolve every dependency in order to inspect each jar's - # license, so it populates exactly what the other jobs need, and it stops - # at `package`, so this project's own artifacts never enter the entry. - # Maven CI Build restores the same key read-only. + # This job is the writer for that cache, and deliberately the only one: + # it is the short job, so making it the writer costs the least, and it + # stops at `package`, so this project's own artifacts never enter the + # entry. Maven CI Build restores the same key read-only. + # + # Being the only writer means this job has to resolve what the readers + # need, which the license steps below do not do on their own - they build + # the shaded modules and their `-am` closure, which excludes the two + # leaves, xtable-utilities and xtable-service. The explicit resolve step + # after them covers the rest of the reactor. # # Restore and save are split so that only `main` writes. The entry is # ~1 GB against a repository-wide 10 GB budget, so letting every branch @@ -101,6 +106,24 @@ jobs: - name: Validate Bundled License Texts run: python3 release/scripts/validate_bundled_license_texts.py + # Nothing above this line resolves xtable-utilities or xtable-service: + # `apache-rat:check` requires no dependency resolution, `dependency:tree` + # collects poms rather than jars, and the shaded-bundle build reaches only + # the `-am` closure of the three modules it names, which excludes both + # leaves. Without this step the entry never gains the Quarkus stack or + # xtable-utilities' bundled dependencies, and Maven CI Build - which + # cannot write the cache - would re-download them from Central on every + # run, permanently. This job exists to warm the cache for that one, so it + # has to cover the whole reactor even where a license check does not. + # + # Failure here must not fail a license check, hence `|| true`: an + # incomplete cache entry costs download time on the next run and nothing + # else. `go-offline` does not catch plugin dependencies resolved at + # execution time, so expect a small delta regardless. + - name: Resolve the rest of the reactor for the cache + if: success() && github.ref == 'refs/heads/main' + run: ./mvnw -B -ntp -q dependency:go-offline -Dmaven.build.cache.enabled=false || true + # This job writes the repository cache, so the invariant that the entry # holds third-party dependencies only is enforced here. The steps above # stop at `package`, so this is a no-op today; it exists so that adding From 95dad45c5d8c24df3233cff6d7e74abde43b06a5 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 10 Aug 2026 23:15:20 +0200 Subject: [PATCH 9/9] Bound the build cache save on both sides The save gated on `size_mb < 100` while the warning fired above 100, so a directory of exactly 100 MB - reachable, du -sm rounds up - was skipped silently, the one outcome the guard exists to make visible. It also treated the `${size_mb:-0}` fallback for a missing directory as a pass, handing actions/cache/save a path that is not there. Saving is now also conditional on the exact key having missed on restore. Cache entries are immutable, so a re-run of a main commit would try to write over its own entry and log "Unable to reserve cache with key" - the message the earlier commits removed by giving the two jobs separate keys. The repository cache save already carries this check. --- .github/workflows/mvn-ci-build.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mvn-ci-build.yml b/.github/workflows/mvn-ci-build.yml index ef32cf4e0..401148331 100644 --- a/.github/workflows/mvn-ci-build.yml +++ b/.github/workflows/mvn-ci-build.yml @@ -83,7 +83,7 @@ jobs: # # The modules producing large bundled jars opt out of the cache entirely # with false in their - # own poms, which keeps this entry around 15 MB. Without that opt-out the + # own poms, which keeps this entry around 12 MB. Without that opt-out the # entry is 1.2 GB, 97% of it three shaded jars that account for only ~3 of # the 27 minutes a cold build takes. # @@ -91,6 +91,7 @@ jobs: # target/quarkus-app. No step consumes that directory today; anything added # later that does must not rely on it being present. - name: Restore Maven build cache + id: build-cache uses: actions/cache/restore@v6 with: path: ~/.m2/build-cache @@ -113,12 +114,24 @@ jobs: size_mb=${size_mb:-0} echo "size_mb=${size_mb}" >> "$GITHUB_OUTPUT" echo "~/.m2/build-cache is ${size_mb} MB" - if [ "${size_mb}" -gt 100 ]; then + # The two bounds below are what the save step gates on, so they have to + # cover the whole range between them: 0 means the extension wrote + # nothing and there is no path for the save action to read. + if [ "${size_mb}" -eq 0 ]; then + echo "::warning title=Maven build cache not saved::~/.m2/build-cache is empty or absent. The build cache extension did not write anything; check that .mvn/extensions.xml is still in effect." + elif [ "${size_mb}" -ge 100 ]; then echo "::warning title=Maven build cache not saved::~/.m2/build-cache is ${size_mb} MB (limit 100 MB). A module is caching large artifacts; opt it out with false in its pom." fi + # Skipped when the exact key already hit on restore - a re-run of the same + # commit would otherwise try to write over an immutable entry and log the + # "Unable to reserve cache with key" that splitting these jobs removed. - name: Save Maven build cache - if: success() && github.ref == 'refs/heads/main' && steps.build-cache-size.outputs.size_mb < 100 + if: > + success() && github.ref == 'refs/heads/main' + && steps.build-cache.outputs.cache-hit != 'true' + && steps.build-cache-size.outputs.size_mb > 0 + && steps.build-cache-size.outputs.size_mb < 100 uses: actions/cache/save@v6 with: path: ~/.m2/build-cache