Skip to content

ci: share the dependency caches between pull requests - #25479

Open
mcollovati wants to merge 2 commits into
mainfrom
ci/share-maven-and-gradle-caches
Open

ci: share the dependency caches between pull requests#25479
mcollovati wants to merge 2 commits into
mainfrom
ci/share-maven-and-gradle-caches

Conversation

@mcollovati

@mcollovati mcollovati commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

A run can only read caches from its own ref and from the default branch. Since
#24103 dropped main from the validation push trigger, nothing writes to
refs/heads/main any more, and a merge queue branch is single use, so its
caches are never read either.

So every pull request starts with an empty local repository, downloads the whole
dependency tree, and then saves a copy only it can read. Those copies do not
fit: 10.7 GB against a 10 GB limit, of which Gradle was 7.1 GB. Entries evict
each other within hours, and even a second run of the same pull request starts
cold. One branch went from 4 minutes to 13.6 minutes in a day without changing.

validation.yml lets gradle/actions/setup-gradle cache the Gradle home,
instead of caching the whole ~/.gradle/caches tree by hand. It keeps
deduplicated entries and, as configured here, only writes on a push, so pull
requests read one instead of each saving 1.4 GB. That alone takes 7.1 GB out of
the limit. The action's default cache provider is a proprietary component that
its MIT licence does not cover and is free only as a preview, so this asks for
the MIT one.

warm-caches.yml is a new workflow that builds main once per merge, so the
default branch holds an entry every pull request can read. It is a single job,
not the validation matrix, so it does not bring back the duplicate validation
that #24103 removed.

The Flow artifacts are removed before the cache is saved. An entry every branch
restores should not carry one branch's 999.99-SNAPSHOT jars, which could satisfy
a resolution that ought to fail.

@mcollovati
mcollovati requested a review from Artur- September 4, 2026 09:55
@caalador

caalador commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Findings, most significant first:

  1. The cache-warming run on main cancels itself — validation.yml:9 with the workflow-level concurrency at :24-25. A push to main resolves to group Flow Validation-main with cancel-in-progress: true, so a second merge landing during the ~30-60 min run kills the first. Neither cache survives cancellation: actions/cache@v5 is post-if: success(), setup-gradle@v6.3.0 is post-if: '!cancelled()'. On a busy day main may never finish a warming run and PRs stay cold. Fix: cancel-in-progress: ${{ github.event_name != 'push' }}, or put the sha in the group for pushes.
  2. cache-read-only default breaks maintenance branches — :391. The action defaults it to "true unless on the default branch", which is correct for main, but the removed actions/cache step wrote from any branch. Once this is forward-merged to 25.2/25.1/…/23.6, pushes to those branches never populate a Gradle cache in their own scope — PRs there fall back to main's entry (different lineage, different JAVA_VERSION) or run cold. Set cache-read-only: ${{ github.event_name != 'push' }} explicitly if maintenance branches should warm their own scope.
  3. cache-provider defaults to enhanced, which is proprietary — :391. Per the action's DISTRIBUTION.md, the vendored gradle-actions-caching component is not covered by the repo's MIT license, is governed by Gradle Technologies' Terms of Use, and prints a notice in every job log. For an OSS repo that should be a deliberate, recorded choice; cache-provider: basic is the MIT implementation and caches ~/.gradle/caches + ~/.gradle/wrapper — closest to the step being removed.
    (Dropping ~/.gradle/wrapper from the paths is not a regression; both providers cache wrapper distributions.)
  4. api-diff-labeling's cache still competes — :632. That job is gated if: github.event_name == 'pull_request' (:610), so its key ${{ runner.os }}-maven-api-diff-${{ hashFiles('**/pom.xml') }} is never written by the main push. Its primary key can never hit, and every PR run still saves a full ~/.m2/repository into its own ref scope — so the 10 GB pressure and LRU eviction this PR is meant to relieve persists, and the victims are the new shared main entries. Use actions/cache/restore there, or a key main also produces.
  5. Every push to main now re-runs the whole suite — :9. Build + 4 unit jobs + the 15-way IT matrix + sonar + the 120-min gradle job, for a tree the merge queue just validated: roughly double CI per merge, and flaky ITs will paint main red. If only cache warming is wanted, a small dedicated warm-cache workflow (or restricting the push run to the cache-producing jobs) gets the same benefit.
  6. Shared Maven entry now carries main's Flow jars — :9. The Linux-maven- entry is saved after "Compile and Install Flow", so it contains main's com/vaadin/*/999.99-SNAPSHOT. Every PR job then starts with those in the local repo, and the workspace.tar restore only overwrites paths it contains — a PR that removes or renames a module can resolve main's stale jar instead of failing. Since the key is an exact hit on later pushes, the entry isn't refreshed until a pom changes, so the jars can be arbitrarily old. Excluding !~/.m2/repository/com/vaadin (as api-diff-labeling already does) removes the hazard.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results

 1 434 files  ± 0   1 518 suites  ±0   1h 35m 20s ⏱️ - 4m 41s
11 865 tests  - 12  11 798 ✅  - 12  67 💤 ±0  0 ❌ ±0 
12 184 runs   - 12  12 116 ✅  - 12  68 💤 ±0  0 ❌ ±0 

Results for commit 08a6998. ± Comparison against base commit 5967a66.

♻️ This comment has been updated with latest results.

@mcollovati

Copy link
Copy Markdown
Collaborator Author
  1. This was considered, but running warm cache workflow skipping tests would prevent a good portion of artifacts from being cached.

  2. The key does hit on a second run of the same pull request, and restore-keys falls back to main's entry otherwise, so the reads are fine.

@mcollovati
mcollovati requested a review from caalador September 4, 2026 11:11
Comment thread .github/workflows/validation.yml Outdated
# that ought to fail, for a module the branch renamed or removed.
path: |
~/.m2/repository
!~/.m2/repository/com/vaadin

@caalador caalador Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!~/.m2/repository/com/vaadin is a no-op, so the stale-jar hazard remains

Head lines 79-81, 127-129, 243-245, 409-411, 514-516 (and pre-existing at 675-677).

actions/cache resolves path through resolvePaths, which calls glob.create(..., {implicitDescendants: false}) — confirmed in the pinned bundle 27d5ce7f at dist/restore/index.js:1548. Under that option a non-wildcard directory pattern yields only the directory itself; the traversal drops children because partialMatch ignores deeper paths, and negation only clears a match on an identical path (result &= ~pattern.match(itemPath)). Same semantics in the pinned bundle at :6223 and :6236.

Run against a fixture .m2 containing both com/vaadin/flow-server/999.99-SNAPSHOT/flow-server.jar and org/apache/commons/commons.jar:

--- PR pattern (dir + negated subdir) ---
[ '.../fakehome/.m2/repository' ] ← exclusion had no effect

--- wildcard form ---
[ '.../repository', '.../repository/com',
'.../repository/org', '.../repository/org/apache',
'.../repository/org/apache/commons',
'.../repository/org/apache/commons/commons.jar' ] ← com/vaadin excluded

Only ~/.m2/repository reaches the tar manifest, and tar recurses, so com/vaadin/** is still saved. The ~ is expanded fine in the negated pattern (negation is stripped before fixupPattern) — the pattern isn't malformed, just inert.

Two ways out: the wildcard form ~/.m2/repository/** + !~/.m2/repository/com/vaadin/**, which works but enumerates every file in a ~100k-entry local repository; or keep path: ~/.m2/repository and delete the directory in a final regular step (rm -rf ~/.m2/repository/com/vaadin), since cache's post step runs after all regular steps. The second is cheaper and needs no glob subtleties.

Seems this is broken also on the API diff

@github-actions github-actions Bot added the +0.0.1 label Sep 4, 2026
caalador
caalador previously approved these changes Sep 4, 2026
@mcollovati

Copy link
Copy Markdown
Collaborator Author

I'll reconsider the separated cache workflow. Validation is getting too complicated.
And actually, the tests modules seem not participating in the cache.

@mcollovati

Copy link
Copy Markdown
Collaborator Author

Restructured the PR and updated the description

Caching it by hand meant every pull request saved its own copy of it.
Those copies held 7 of the 10 GB the repository is allowed, so entries
evicted each other within hours and even a second run of the same pull
request started cold. Leaving the writing to pushes is what fixes that;
the maintenance branches keep filling their own scope.

The action's default cache provider is a proprietary component that its
MIT licence does not cover and is free only as a preview, so this asks
for the MIT one. That provider saves one entry over ~/.gradle/caches and
~/.gradle/wrapper, the same tree the hand written step cached, and
deliberately passes no restore keys: a change to any build script or
wrapper file now starts from an empty Gradle home rather than a near
match, where the replaced step fell back on a prefix.
@mcollovati
mcollovati force-pushed the ci/share-maven-and-gradle-caches branch from fafa644 to efe3512 Compare September 4, 2026 15:00
A run can only read caches from its own ref and from the default branch.
Since #24103 dropped main from the validation push trigger, nothing
writes to refs/heads/main any more, and a merge queue branch is single
use, so its caches are never read either. Every pull request starts with
an empty local repository, downloads the whole dependency tree, and saves
a copy only it can read. One branch went from 4 minutes to 13.6 minutes
in a day without changing.

Putting main back in the validation trigger would fix that, but it also
brings back the second full run per merge that #24103 removed: the build,
four unit jobs, a fifteen way integration matrix and Sonar, for a tree
the merge queue has just validated, with flaky tests now able to redden
main. A single job gets the same cache: the build job is what fills an
empty local repository, and every other job waits on it, so the entry it
saves is the one they all read anyway.

The Flow artifacts are removed before the cache is saved, since an entry
every branch restores should not carry one branch's 999.99-SNAPSHOT jars,
which could satisfy a resolution that ought to fail.
@mcollovati
mcollovati force-pushed the ci/share-maven-and-gradle-caches branch from efe3512 to 08a6998 Compare September 4, 2026 15:09
@mcollovati

Copy link
Copy Markdown
Collaborator Author

On the cancelled run: cancelling does not remove the entry saved before it. The
restore keys still match that one, so a run that loses a race leaves every
branch on a cache one merge behind, not on no cache at all.

Queueing instead would build one superseded commit after another while the run
that matters waits behind them.

And there is usually time to finish. Over the last 18 days the median gap
between pushes to main was 54 minutes, and 72% of the gaps were longer than 15
minutes.

@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants