Skip to content

Commit 5138206

Browse files
committed
perf(ci): collapse remaining per-PR sticky-disk minters to O(1) keys
The installation hit Blacksmith's backing-disk cap because sticky keys embedded PR numbers and content hashes, minting a new disk per PR and per input change until every mount 429-failed fleet-wide. node-deps (#109752) and ext-boundary (#109804) were already re-keyed; this converts the last two minters and audits the third suspect. Keys changed: - Vitest fs transform cache: `vitest-fs-v2-<pr-N|protected>-...` -> the one existing `vitest-fs-v2-protected-<os>-<arch>-node-<ver>` disk. Entries are content-hash keyed (vitest sha1 over id+content+NODE_ENV+version+env config), so cross-PR sharing is safe by construction: PRs now mount the protected snapshot directly (read-only, commit false) and the separate PR-only seed mount is deleted. Writers (non-PR shard writer + scheduled warm run) keep explicit commit true. Disk count: 1 + O(open PRs) -> 1. - Gradle: `gradle-v1-<task>-<pr-N|protected>-<hashFiles(deps)>` -> `gradle-v2-<task>`. Task scope stays (light ktlint must not seed heavy build lanes); PR number and dependency hash leave the key. The dependency hash moved to an in-job fingerprint marker that makes the non-PR writer rebuild its snapshot cold when inputs change, bounding disk growth; PR mounts are read-only and safely reuse stale snapshots because Gradle caches are content-addressed. Disk count: O(tasks x PRs x dependency bumps) -> O(tasks) (7 today). - Docker builder (useblacksmith/setup-docker-builder): audited, out of scope. The action owns its key internally and always uses the repo name only (src/setup_builder.ts getStickyDisk), so it is already one disk per installation repo and cannot mint per-PR disks. PR warm-path behavior changes: - Vitest: PRs lose only PR-local warm entries for files the PR itself changed (previously persisted on the pr-N disk between pushes); changed files re-transform once per push, unchanged files still hit the protected snapshot. PRs touching transform inputs (lockfile/tsconfig/package.json) run cold per push since the generation wipe is now local to the discarded clone. - Gradle: dependency-bump PRs get warmer (stale-but-valid protected snapshot instead of a cold fresh key); other PRs are unchanged. Deleted `.github/workflows/pr-cache-cleanup.yml`: it existed for the per-PR cache layer (#109425) and only deleted GitHub actions/cache archives, which GitHub's own LRU/TTL eviction already handles for the remaining fork/Windows per-PR archive paths. Guards: pinned the O(1) vitest key + non-PR commit gate, added a Gradle per-task key/writer/fingerprint guard, added a repo-wide scan asserting no useblacksmith/stickydisk key ever contains github.event.pull_request.number or hashFiles(), and replaced the cleanup-workflow assertions with a stays-deleted check.
1 parent dd54c18 commit 5138206

4 files changed

Lines changed: 149 additions & 88 deletions

File tree

.github/actions/setup-node-env/action.yml

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ inputs:
6666
save-vitest-fs-cache:
6767
description: >
6868
Whether this job may save the shared Vitest filesystem module cache.
69-
Other matrix jobs restore the warm seed without committing competing
70-
snapshots.
69+
Only honored outside pull_request events for sticky disks; PR jobs
70+
always mount the protected snapshot read-only.
7171
required: false
7272
default: "false"
7373
build-all-cache-scope:
@@ -144,27 +144,24 @@ runs:
144144
# cannot seed this key.
145145
commit: ${{ inputs.save-sticky-disk == 'true' && github.event_name != 'pull_request' && 'true' || 'false' }}
146146

147-
- name: Mount protected Vitest transform seed
148-
if: inputs.vitest-fs-cache == 'true' && (inputs.sticky-disk == 'true' || inputs.runtime-cache-sticky-disk == 'true') && github.event_name == 'pull_request' && runner.os != 'Windows'
149-
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
150-
with:
151-
# Same-repository PRs read the protected snapshot but never write it.
152-
# Content-addressed misses stay in the PR-scoped overlay below.
153-
key: ${{ github.repository }}-vitest-fs-v2-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}
154-
path: /var/tmp/openclaw-vitest-fs-protected-seed
155-
commit: false
156-
157147
- name: Mount Vitest transform cache sticky disk
158148
if: inputs.vitest-fs-cache == 'true' && (inputs.sticky-disk == 'true' || inputs.runtime-cache-sticky-disk == 'true') && runner.os != 'Windows'
159149
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
160150
with:
161-
# One stable disk per trust scope. The generation guard below clears
151+
# One stable disk per runner shape. The old per-PR overlay disks
152+
# minted a new backing disk for every PR and helped saturate
153+
# Blacksmith's installation-wide sticky-disk budget, 429-failing every
154+
# mount. Cache entries are content-hash keyed (id+content+env), so PRs
155+
# can safely read the protected snapshot directly; PR-local misses are
156+
# rebuilt in the discarded mount. The generation guard below clears
162157
# incompatible transform inputs without leaking one disk per lockfile.
163-
key: ${{ github.repository }}-vitest-fs-v2-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'protected' }}-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}
158+
key: ${{ github.repository }}-vitest-fs-v2-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}
164159
path: /var/tmp/openclaw-vitest-fs-cache
165-
# One semantic writer only. Explicit true avoids stickydisk's allocated-
166-
# byte heuristic missing equal-size replacements or prune-balanced writes.
167-
commit: ${{ inputs.save-vitest-fs-cache == 'true' && 'true' || 'false' }}
160+
# Single semantic writer: only the designated non-PR writer commits;
161+
# pull_request mounts stay read-only clones. Explicit true avoids
162+
# stickydisk's allocated-byte heuristic missing equal-size
163+
# replacements or prune-balanced writes.
164+
commit: ${{ inputs.save-vitest-fs-cache == 'true' && github.event_name != 'pull_request' && 'true' || 'false' }}
168165

169166
- name: Restore and save Vitest transform cache
170167
if: inputs.vitest-fs-cache == 'true' && inputs.sticky-disk != 'true' && inputs.runtime-cache-sticky-disk != 'true' && inputs.save-vitest-fs-cache == 'true' && runner.os != 'Windows'
@@ -190,30 +187,28 @@ runs:
190187
if: inputs.vitest-fs-cache == 'true' && runner.os != 'Windows'
191188
env:
192189
CACHE_GENERATION: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '**/package.json', '**/tsconfig*.json', 'vitest.config.*', 'test/vitest/**') }}
193-
CACHE_WRITER: ${{ inputs.save-vitest-fs-cache == 'true' && '1' || '0' }}
190+
# Sticky PR mounts never commit, so pruning there would only burn
191+
# shard wall clock on a discarded clone; non-sticky (actions/cache)
192+
# writers still prune their PR-scoped archives.
193+
CACHE_WRITER: ${{ inputs.save-vitest-fs-cache == 'true' && ((inputs.sticky-disk != 'true' && inputs.runtime-cache-sticky-disk != 'true') || github.event_name != 'pull_request') && '1' || '0' }}
194194
shell: bash
195195
run: |
196196
set -euo pipefail
197197
cache_root=/var/tmp/openclaw-vitest-fs-cache
198-
seed_root=/var/tmp/openclaw-vitest-fs-protected-seed
199198
generation_file="$cache_root/.openclaw-transform-generation"
200199
mkdir -p "$cache_root"
201200
cache_generation=""
202201
if [[ -f "$generation_file" ]]; then
203202
cache_generation="$(<"$generation_file")"
204203
fi
205204
cache_entry="$(find "$cache_root" -mindepth 1 -maxdepth 1 -print -quit)"
205+
# Read-only PR mounts clear a mismatched generation too: the wipe only
206+
# affects the discarded local clone, and mixing generations could
207+
# false-hit transforms produced under incompatible tsconfig/lockfile.
206208
if [[ -n "$cache_entry" ]] && [[ "$cache_generation" != "$CACHE_GENERATION" ]]; then
207209
echo "Vitest transform inputs changed; clearing incompatible cache generation"
208210
find "$cache_root" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
209211
fi
210-
seed_generation_file="$seed_root/.openclaw-transform-generation"
211-
if [[ -f "$seed_generation_file" ]] && [[ "$(<"$seed_generation_file")" == "$CACHE_GENERATION" ]]; then
212-
echo "Merging matching protected Vitest transform seed"
213-
cp -a --no-clobber "$seed_root"/. "$cache_root"/
214-
elif [[ -f "$seed_generation_file" ]]; then
215-
echo "Ignoring protected Vitest transform seed from an incompatible generation"
216-
fi
217212
printf '%s\n' "$CACHE_GENERATION" > "$generation_file"
218213
# The shard runner treats this as a persistent root and assigns one
219214
# isolated subdirectory per concurrent Vitest worker.

.github/workflows/ci.yml

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,8 +1833,9 @@ jobs:
18331833
vitest-fs-cache: "true"
18341834
node-compile-cache: "true"
18351835
node-compile-cache-scope: "test"
1836-
# One matrix job commits the shared warm seed. Other jobs restore it
1837-
# read-only, avoiding concurrent sticky-disk or archive writers.
1836+
# One matrix job is the designated warm-seed writer; the action only
1837+
# honors it outside pull_request events, so PR shards always read the
1838+
# protected snapshot without committing competing writes.
18381839
save-vitest-fs-cache: ${{ matrix.save_vitest_fs_cache && 'true' || 'false' }}
18391840
save-node-compile-cache: ${{ matrix.save_vitest_fs_cache && 'true' || 'false' }}
18401841

@@ -3135,28 +3136,54 @@ jobs:
31353136
if: github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw')
31363137
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
31373138
with:
3138-
# Task-scoped keys keep one semantic writer per snapshot: each matrix
3139-
# task resolves a different dependency set, and a shared if-missing
3140-
# key would let a light task (ktlint) win the cold-key race and pin a
3141-
# thin snapshot the heavy build lanes can never repair. Gradle's own
3142-
# caches are content-addressed, so stale restored entries are ignored
3143-
# and dependency-file changes mint a fresh key/snapshot.
3144-
key: ${{ github.repository }}-gradle-v1-${{ matrix.task }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'protected' }}-${{ hashFiles('apps/android/**/*.gradle*', 'apps/android/**/gradle-wrapper.properties', 'apps/android/gradle/libs.versions.toml') }}
3139+
# One stable disk per matrix task. The v1 per-PR/per-dependency-hash
3140+
# keys minted a new backing disk for every PR and dependency bump
3141+
# and helped saturate Blacksmith's installation-wide sticky-disk
3142+
# budget, 429-failing every mount. Task scope stays in the key: each
3143+
# matrix task resolves a different dependency set, and a shared key
3144+
# would let a light task (ktlint) pin a thin snapshot the heavy
3145+
# build lanes can never repair. Dependency-file changes live in the
3146+
# runtime fingerprint marker below instead of the key.
3147+
key: ${{ github.repository }}-gradle-v2-${{ matrix.task }}
31453148
path: /var/tmp/openclaw-gradle
3146-
# on-change keeps the snapshot learning: Gradle's build cache
3147-
# accumulates across runs (if-missing would freeze the first
3148-
# hydration forever). Entries are content-addressed, task-scoped
3149-
# keys give one semantic writer, and v1.4.0 still skips commit
3150-
# after failed/cancelled steps.
3151-
commit: on-change
3149+
# Single semantic writer: only protected pushes commit, so
3150+
# pull_request clones stay read-only and the snapshot tracks main.
3151+
# Explicit true (not on-change) because the allocated-byte heuristic
3152+
# can miss a same-size refresh and strand the fingerprint marker;
3153+
# v1.4.0 still skips commit after failed/cancelled steps, so a
3154+
# broken build cannot poison this key.
3155+
commit: ${{ github.event_name != 'pull_request' && 'true' || 'false' }}
31523156

31533157
- name: Point Gradle at the sticky disk
31543158
if: github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw')
31553159
shell: bash
3160+
env:
3161+
# Gradle caches are content-addressed, so a stale snapshot is safe
3162+
# to restore (obsolete entries are simply ignored). The fingerprint
3163+
# only bounds disk growth: when dependency inputs change, the writer
3164+
# rebuilds its snapshot from scratch so retired dependency artifacts
3165+
# do not accumulate on the O(1) key forever.
3166+
GRADLE_DEPS_FINGERPRINT: ${{ hashFiles('apps/android/**/*.gradle*', 'apps/android/**/gradle-wrapper.properties', 'apps/android/gradle/libs.versions.toml') }}
3167+
STICKY_WRITER: ${{ github.event_name != 'pull_request' && 'true' || 'false' }}
31563168
run: |
31573169
set -euo pipefail
3158-
mkdir -p /var/tmp/openclaw-gradle/gradle-user-home
3159-
echo "GRADLE_USER_HOME=/var/tmp/openclaw-gradle/gradle-user-home" >> "$GITHUB_ENV"
3170+
sticky_root=/var/tmp/openclaw-gradle
3171+
marker="$sticky_root/.openclaw-gradle-deps-fingerprint"
3172+
disk_fingerprint=""
3173+
if [ -f "$marker" ]; then
3174+
disk_fingerprint="$(<"$marker")"
3175+
fi
3176+
if [ "$disk_fingerprint" != "$GRADLE_DEPS_FINGERPRINT" ]; then
3177+
if [ "$STICKY_WRITER" = "true" ]; then
3178+
echo "Gradle dependency inputs changed; rebuilding snapshot cold to drop retired artifacts"
3179+
rm -rf "$sticky_root/gradle-user-home"
3180+
printf '%s\n' "$GRADLE_DEPS_FINGERPRINT" > "$marker"
3181+
else
3182+
echo "Gradle dependency inputs changed since snapshot; content-addressed caches stay safe to reuse"
3183+
fi
3184+
fi
3185+
mkdir -p "$sticky_root/gradle-user-home"
3186+
echo "GRADLE_USER_HOME=$sticky_root/gradle-user-home" >> "$GITHUB_ENV"
31603187
31613188
- name: Run Android ${{ matrix.task }}
31623189
working-directory: apps/android

.github/workflows/pr-cache-cleanup.yml

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)