mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 20:01:09 +00:00
ci: bound Blacksmith sticky disk growth (#109934)
This commit is contained in:
committed by
GitHub
parent
e316e1c440
commit
00a64f6e95
15
.github/actions/setup-node-env/action.yml
vendored
15
.github/actions/setup-node-env/action.yml
vendored
@@ -124,12 +124,10 @@ runs:
|
||||
if: inputs.sticky-disk == 'true'
|
||||
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
|
||||
with:
|
||||
# One stable disk per node-version. The v2 per-PR/per-manifest-hash
|
||||
# keys minted a new backing disk for every PR and dependency change
|
||||
# and saturated Blacksmith's installation-wide sticky-disk budget,
|
||||
# 429-failing every mount. Install inputs live in the runtime
|
||||
# fingerprint marker below instead of the key, so dependency changes
|
||||
# refresh this disk in place rather than allocating a new one.
|
||||
# One stable disk per Node line. The v2 per-PR/per-manifest-hash keys
|
||||
# saturated Blacksmith's installation-wide sticky-disk budget. Install
|
||||
# inputs, runner platform, and the exact Node patch live in the runtime
|
||||
# marker below, so changes refresh this disk in place.
|
||||
key: ${{ github.repository }}-node-deps-bind-v3-${{ inputs.node-version }}
|
||||
path: /var/tmp/openclaw-node-deps
|
||||
# Single semantic writer: only the designated trusted-push job may
|
||||
@@ -273,7 +271,7 @@ runs:
|
||||
# the '**/package.json' glob only sees checkout manifests. Recomputing
|
||||
# this hash any later would sweep snapshot-internal package.json files
|
||||
# into the fingerprint and permanently miss the warm path.
|
||||
DEPS_FINGERPRINT: node-${{ inputs.node-version }}-frozen-${{ inputs.frozen-lockfile }}-${{ hashFiles('**/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', 'pnpmfile.cjs', '.github/actions/setup-node-env/sticky-importers.sh', 'scripts/postinstall-bundled-plugins.mjs', 'scripts/preinstall-package-manager-warning.mjs', 'scripts/prepare-git-hooks.mjs') }}
|
||||
DEPS_INPUT_FINGERPRINT: frozen-${{ inputs.frozen-lockfile }}-${{ hashFiles('**/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', 'pnpmfile.cjs', '.github/actions/setup-node-env/sticky-importers.sh', 'scripts/postinstall-bundled-plugins.mjs', 'scripts/preinstall-package-manager-warning.mjs', 'scripts/prepare-git-hooks.mjs') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sticky_root=/var/tmp/openclaw-node-deps
|
||||
@@ -289,9 +287,10 @@ runs:
|
||||
exit 1
|
||||
fi
|
||||
findmnt --target "$workspace_modules"
|
||||
deps_fingerprint="os-${RUNNER_OS:?}-arch-${RUNNER_ARCH:?}-node-$(node --version)-${DEPS_INPUT_FINGERPRINT:?}"
|
||||
# zizmor: ignore[github-env] static trusted path defined in this composite action.
|
||||
echo "PNPM_CONFIG_STORE_DIR=$sticky_store" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_STICKY_DEPS_FINGERPRINT=$DEPS_FINGERPRINT" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_STICKY_DEPS_FINGERPRINT=$deps_fingerprint" >> "$GITHUB_ENV"
|
||||
# pnpm exec may reconcile a restored workspace before nested builds.
|
||||
# Sticky jobs already have every build tool, so invoke their Node entrypoints directly.
|
||||
echo "OPENCLAW_BUILD_ALL_NO_PNPM=1" >> "$GITHUB_ENV"
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -1021,10 +1021,6 @@ jobs:
|
||||
# repository-global state and must never be produced by fork code.
|
||||
sticky-disk: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }}
|
||||
use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }}
|
||||
# Designated dependency-snapshot writer: exactly one trusted-push job
|
||||
# per node-version key refreshes the protected disk; every other
|
||||
# sticky consumer mounts read-only.
|
||||
save-sticky-disk: "true"
|
||||
runtime-cache-sticky-disk: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }}
|
||||
save-node-compile-cache: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'true' || 'false' }}
|
||||
|
||||
@@ -1827,9 +1823,9 @@ jobs:
|
||||
# Blacksmith same-repo runs clone dependencies from a sticky disk.
|
||||
# Fork PRs must keep actions/cache: sticky snapshots are writable,
|
||||
# repository-global state and must never be produced by fork code.
|
||||
sticky-disk: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }}
|
||||
runtime-cache-sticky-disk: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }}
|
||||
use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }}
|
||||
sticky-disk: ${{ (matrix.node_version == null || matrix.node_version == '24.x') && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }}
|
||||
runtime-cache-sticky-disk: ${{ (matrix.node_version == null || matrix.node_version == '24.x') && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }}
|
||||
use-actions-cache: ${{ (matrix.node_version == null || matrix.node_version == '24.x') && github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }}
|
||||
vitest-fs-cache: "true"
|
||||
node-compile-cache: "true"
|
||||
node-compile-cache-scope: "test"
|
||||
|
||||
25
.github/workflows/vitest-cache-warm.yml
vendored
25
.github/workflows/vitest-cache-warm.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
types: [vitest-cache-warm]
|
||||
schedule:
|
||||
- cron: "17 8 * * *"
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers] successful main-push CI only; read-only default-branch checkout, no artifacts or untrusted ref
|
||||
workflows: [CI]
|
||||
branches: [main]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -15,7 +19,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
warm:
|
||||
if: github.repository == 'openclaw/openclaw'
|
||||
if: github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_run' || (github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main'))
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
@@ -26,21 +30,21 @@ jobs:
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
node-compile-cache: "true"
|
||||
node-compile-cache: ${{ github.event_name != 'workflow_run' && 'true' || 'false' }}
|
||||
node-compile-cache-scope: "test"
|
||||
runtime-cache-sticky-disk: "true"
|
||||
save-node-compile-cache: "true"
|
||||
# Daily deadline writer for the protected dependency snapshot:
|
||||
# canonical main pushes cancel each other under merge traffic, so
|
||||
# build-artifacts commits can starve; this scheduled run cannot be
|
||||
# superseded and keeps the snapshot within a day of main.
|
||||
runtime-cache-sticky-disk: ${{ github.event_name != 'workflow_run' && 'true' || 'false' }}
|
||||
save-node-compile-cache: ${{ github.event_name != 'workflow_run' && 'true' || 'false' }}
|
||||
# This disposable, non-cancelled workflow is the sole dependency
|
||||
# snapshot writer. Main CI completion coalesces refreshes without
|
||||
# putting required build jobs in a cancel-on-pending mutex.
|
||||
save-sticky-disk: "true"
|
||||
save-vitest-fs-cache: "true"
|
||||
save-vitest-fs-cache: ${{ github.event_name != 'workflow_run' && 'true' || 'false' }}
|
||||
sticky-disk: "true"
|
||||
use-actions-cache: "false"
|
||||
vitest-fs-cache: "true"
|
||||
vitest-fs-cache: ${{ github.event_name != 'workflow_run' && 'true' || 'false' }}
|
||||
|
||||
- name: Select broad cache seed
|
||||
if: github.event_name != 'workflow_run'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -68,4 +72,5 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Warm transform and compile caches
|
||||
if: github.event_name != 'workflow_run'
|
||||
run: node scripts/ci-run-node-test-shard.mjs
|
||||
|
||||
@@ -114,7 +114,8 @@ The slowest Node test families are split or balanced so each job stays small wit
|
||||
- Pull requests on the canonical repository reuse the changed-test resolver against the synthetic merged-tree diff. Precise changes run one targeted Node job; each selected test file gets its own process so stateful suite isolation remains intact. The planner combines sibling tests with import-graph dependents and falls back to the existing 14-job compact full-suite plan for workspace package, package/lockfile, shared harness, split-config, renamed, or deleted changes, public extension-contract changes, tests with special shard setup, partially resolved or empty targets, oversized path or target plans, and planner errors. Targeted plans always retain the full built-artifact boundary gate because its repository scanners cannot be derived from imports. `main` pushes, manual dispatches, and release gates retain the full matrix because canceled superseded `main` runs make a single-push diff insufficient as integration proof.
|
||||
- The full Node matrix admits the consistently slow serial tooling, auto-reply command shards, and broad core-fast cache writer first. This keeps the 28-job cap while preventing critical-path work and the next run's transform seed from slipping into a later wave.
|
||||
- Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard.
|
||||
- Linux Node shard jobs persist Vitest's experimental filesystem module cache. Trusted Blacksmith jobs use PR-scoped writable overlays seeded from the protected snapshot only when their transform-input generations match; GitHub-hosted and fork jobs use an `actions/cache` fallback with coarse restore prefixes. The planner marks the broad `core-unit-fast` graph as the single writer without coupling cache ownership to matrix order, while every other job restores a private read-only clone. Concurrent Vitest workers retain separate live directories. A transform-input fingerprint clears incompatible lockfile, package, tsconfig, and Vitest-config generations inside stable sticky keys. Only the writer scans and prunes the cache to 75% after it exceeds 2 GiB. A non-cancelling daily or default-branch repository-dispatch warmer prevents rapid `main` pushes from starving the protected seed, and closed PR cache archives are deleted.
|
||||
- Linux Node shard jobs persist Vitest's experimental filesystem module cache. Trusted Blacksmith jobs clone one protected disk per platform and Node line; pull requests write only to their private clone and discard it, so PR traffic cannot allocate backing disks or publish feature-branch transforms. GitHub-hosted and fork jobs use an `actions/cache` fallback with coarse PR-scoped restore prefixes. The planner marks the broad `core-unit-fast` graph as the single writer without coupling cache ownership to matrix order. Concurrent Vitest workers retain separate live directories. A transform-input fingerprint clears incompatible lockfile, package, tsconfig, and Vitest-config generations inside the stable disk. Only a protected writer scans and prunes the cache to 75% after it exceeds 2 GiB. A non-cancelling daily or default-branch repository-dispatch warmer refreshes the protected seed; GitHub's normal cache eviction expires fallback PR archives.
|
||||
- Trusted Linux Node jobs also bind the pnpm store and `node_modules` from one protected dependency disk per supported Node line. Package manifests, install settings, runner platform, and the exact Node patch stay out of the disk key; an exact runtime and install-input fingerprint decides whether a job reuses the tree or reinstalls and refreshes the same disk. The non-cancelled cache warmer is the only writer: successful `main` CI completion coalesces a dependency refresh, while the daily run remains a deadline fallback. Required CI jobs and pull requests get disposable clones, so dependency changes do not create new disks, competing snapshots, or a cache lock that can cancel builds.
|
||||
- Node shard and build-artifact jobs also restore Node's portable on-disk compile cache. Independent `test` and `build` namespaces prevent their writers from replacing each other's snapshots: the scheduled test warmer owns the protected test seed, while `build-artifacts` publishes the protected build seed only from trusted `main` pushes. PR jobs read protected snapshots without publishing feature-branch bytecode; fallback archives remain PR-scoped. This reuses V8 bytecode for Node-loaded orchestration, build tooling, and external dependencies across different checkout paths, including when only part of the source graph changes. Vitest child processes disable an inherited compile cache because coverage can be enabled inside dynamic configs and V8 coverage can lose source-position precision when scripts are deserialized from bytecode.
|
||||
- The build-artifact job also persists content-fingerprinted `build-all` step outputs. CI's self-built plugin SDK declarations hash the complete repository-owned TypeScript/JSON source graph, exclude installed and generated directories, and restore both flat declarations and package bridges after `tsdown` clears `dist`. Documentation, workflow, plugin, and other changes outside that graph can reuse the declaration snapshot; source changes rebuild it before the export gate runs.
|
||||
- Full declaration builds split `tsdown` into AI, workspace-package, and unified groups. Each group caches declarations only, then still rebuilds runtime JavaScript before restoring those declarations. Core or plugin changes therefore invalidate only the large unified graph, while workspace-package changes conservatively invalidate every dependent declaration group. Public full builds generally use an immutable Actions cache; coarse restore keys seed partial changes, per-group content fingerprints reject stale data, and GitHub's cache quota evicts old generations. The weekly Node 22 lane instead publishes a 14-day artifact after successful `main` runs and restores only artifacts whose immutable producer identity resolves to that workflow on `main`, avoiding quota churn without allowing PR code to write a shared cache. Private-QA declarations are never persisted in Actions caches because cache namespaces are not confidentiality boundaries.
|
||||
@@ -127,7 +128,9 @@ those runner pools are narrower. Compact whole-config batches run with a
|
||||
120-minute batch timeout, while include-pattern groups share the same bounded
|
||||
job budget.
|
||||
|
||||
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
|
||||
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push. Each current Gradle task has one protected sticky disk; PR jobs use disposable clones, while protected runs refresh content-addressed Gradle entries in place.
|
||||
|
||||
Blacksmith sticky-disk keys are deliberately bounded by supported runtime or task dimensions, never PR number, commit, run, branch, or dependency hash. After a key-version migration, maintainers remove obsolete entries in Blacksmith's Sticky Disks dashboard; GitHub Actions fallback archives use GitHub's normal cache eviction.
|
||||
|
||||
The `check-dependencies` shard runs production Knip dependency, unused-file, and unused-export checks. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. The unused-export guard excludes test-support files and fails on every unused production export; intentional dynamic consumers must be modeled in `config/knip.config.ts`. Historical targets run the export guard when they provide it and retain their older dead-code fallback otherwise.
|
||||
|
||||
|
||||
@@ -1925,15 +1925,13 @@ describe("ci workflow guards", () => {
|
||||
);
|
||||
expect(cacheCondition, jobName).toContain("&& 'false' || 'true'");
|
||||
}
|
||||
// Exactly one CI job may publish the shared snapshot; concurrent
|
||||
// committers would race the disk and consumers could clone a torn tree.
|
||||
const ciWriters = stickyConsumers.filter(
|
||||
(entry) => entry.stepWith["save-sticky-disk"] === "true",
|
||||
);
|
||||
expect(ciWriters.map((entry) => entry.jobName)).toEqual(["build-artifacts"]);
|
||||
// The disk key is partitioned by node-version and both writers rely on
|
||||
// the action default; a consumer pinning any other version would split
|
||||
// onto a writerless key and silently regress to permanently cold installs.
|
||||
// Required CI jobs only clone the snapshot. The disposable warmer below
|
||||
// owns commits so writer coalescing cannot cancel a required build job.
|
||||
for (const { jobName, stepWith } of stickyConsumers) {
|
||||
expect(stepWith["save-sticky-disk"], jobName).toBeUndefined();
|
||||
}
|
||||
// Current sticky consumers all use the single supported Node line. A
|
||||
// planner-provided version would silently create a writerless disk.
|
||||
for (const { jobName, stepWith } of stickyConsumers) {
|
||||
const nodeVersion = stepWith["node-version"];
|
||||
expect(
|
||||
@@ -1942,17 +1940,30 @@ describe("ci workflow guards", () => {
|
||||
nodeVersion === "${{ matrix.node_version || '24.x' }}",
|
||||
`${jobName} must resolve to the writer's 24.x snapshot key (got ${String(nodeVersion)})`,
|
||||
).toBe(true);
|
||||
if (nodeVersion === "${{ matrix.node_version || '24.x' }}") {
|
||||
expect(stepWith["sticky-disk"], jobName).toContain(
|
||||
"matrix.node_version == null || matrix.node_version == '24.x'",
|
||||
);
|
||||
expect(stepWith["runtime-cache-sticky-disk"], jobName).toContain(
|
||||
"matrix.node_version == null || matrix.node_version == '24.x'",
|
||||
);
|
||||
}
|
||||
}
|
||||
const warmWorkflow = parse(readFileSync(".github/workflows/vitest-cache-warm.yml", "utf8"));
|
||||
const warmSetupStep = warmWorkflow.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Setup Node environment",
|
||||
);
|
||||
// The scheduled warm run is the deadline writer: canonical main pushes
|
||||
// cancel each other under merge traffic, so build-artifacts alone could
|
||||
// starve the snapshot for days.
|
||||
expect(warmSetupStep.with["save-sticky-disk"]).toBe("true");
|
||||
expect(warmWorkflow.on).not.toHaveProperty("pull_request");
|
||||
expect(warmWorkflow.on).not.toHaveProperty("workflow_dispatch");
|
||||
expect(warmWorkflow.on.workflow_run).toMatchObject({
|
||||
workflows: ["CI"],
|
||||
branches: ["main"],
|
||||
types: ["completed"],
|
||||
});
|
||||
expect(warmWorkflow.jobs.warm.if).toContain(
|
||||
"github.event.workflow_run.conclusion == 'success'",
|
||||
);
|
||||
const action = parse(readFileSync(".github/actions/setup-node-env/action.yml", "utf8"));
|
||||
const validateLayoutStep = action.runs.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Validate sticky pnpm layout",
|
||||
@@ -2005,9 +2016,9 @@ describe("ci workflow guards", () => {
|
||||
path: "/var/tmp/openclaw-node-deps",
|
||||
},
|
||||
});
|
||||
// O(1) disks: Blacksmith caps sticky disks per installation, and the old
|
||||
// per-PR/per-manifest-hash keys saturated that cap (mounts 429ed fleet
|
||||
// wide). Install inputs belong in the runtime fingerprint, not the key.
|
||||
// Bounded disks: Blacksmith caps sticky disks per installation, and the old
|
||||
// per-PR/per-manifest-hash keys saturated that cap. Install inputs and exact
|
||||
// runtime patches belong in the marker, not the backing-disk key.
|
||||
expect(mountStep.with.key).toBe(
|
||||
"${{ github.repository }}-node-deps-bind-v3-${{ inputs.node-version }}",
|
||||
);
|
||||
@@ -2028,14 +2039,16 @@ describe("ci workflow guards", () => {
|
||||
expect(bindStep.run).toContain('sudo mount --bind "$sticky_modules" "$workspace_modules"');
|
||||
expect(bindStep.run).toContain('echo "PNPM_CONFIG_STORE_DIR=$sticky_store"');
|
||||
expect(bindStep.run).toContain('echo "OPENCLAW_BUILD_ALL_NO_PNPM=1"');
|
||||
expect(bindStep.run).toContain('echo "OPENCLAW_STICKY_DEPS_FINGERPRINT=$DEPS_FINGERPRINT"');
|
||||
expect(bindStep.run).toContain(
|
||||
'deps_fingerprint="os-${RUNNER_OS:?}-arch-${RUNNER_ARCH:?}-node-$(node --version)-${DEPS_INPUT_FINGERPRINT:?}"',
|
||||
);
|
||||
expect(bindStep.run).toContain('echo "OPENCLAW_STICKY_DEPS_FINGERPRINT=$deps_fingerprint"');
|
||||
expect(bindStep.run).not.toContain("PNPM_CONFIG_MODULES_DIR");
|
||||
expect(bindStep.run).not.toContain("PNPM_CONFIG_VIRTUAL_STORE_DIR");
|
||||
// The fingerprint must be evaluated on this step (before its bind mount
|
||||
// lands): any later hashFiles('**/package.json') sweep would include
|
||||
// snapshot-internal manifests and permanently miss the warm path.
|
||||
const fingerprintEnv = bindStep.env.DEPS_FINGERPRINT;
|
||||
expect(fingerprintEnv).toContain("node-${{ inputs.node-version }}");
|
||||
const fingerprintEnv = bindStep.env.DEPS_INPUT_FINGERPRINT;
|
||||
expect(fingerprintEnv).toContain("frozen-${{ inputs.frozen-lockfile }}");
|
||||
expect(fingerprintEnv).toContain("hashFiles('**/package.json', 'pnpm-lock.yaml'");
|
||||
expect(fingerprintEnv).toContain("'pnpm-workspace.yaml'");
|
||||
@@ -2253,6 +2266,11 @@ describe("ci workflow guards", () => {
|
||||
// helped exhaust Blacksmith's installation-wide sticky-disk budget.
|
||||
// Content-hash entry keys make cross-PR sharing safe by construction, so
|
||||
// every PR reads the one protected snapshot instead.
|
||||
expect(
|
||||
action.runs.steps.some(
|
||||
(step: WorkflowStep) => step.name === "Mount protected Vitest transform seed",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(stickyStep).toMatchObject({
|
||||
uses: "useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f",
|
||||
with: {
|
||||
@@ -2264,6 +2282,8 @@ describe("ci workflow guards", () => {
|
||||
},
|
||||
});
|
||||
expect(stickyStep.if).toContain("inputs.sticky-disk == 'true'");
|
||||
expect(stickyStep.with.key).not.toContain("pull_request");
|
||||
expect(stickyStep.with.key).not.toContain("hashFiles");
|
||||
expect(writerStep.uses).toBe("actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae");
|
||||
expect(writerStep.if).toContain("inputs.save-vitest-fs-cache == 'true'");
|
||||
expect(writerStep.with.key).toContain("github.run_id");
|
||||
@@ -2274,6 +2294,10 @@ describe("ci workflow guards", () => {
|
||||
expect(readerStep.with["restore-keys"]).toBe(writerStep.with["restore-keys"]);
|
||||
expect(configureStep.run).toContain("OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=$cache_root");
|
||||
expect(configureStep.run).toContain(".openclaw-transform-generation");
|
||||
expect(configureStep.run).not.toContain("protected Vitest transform seed");
|
||||
expect(configureStep.env.CACHE_WRITER).toBe(
|
||||
"${{ inputs.save-vitest-fs-cache == 'true' && ((inputs.sticky-disk != 'true' && inputs.runtime-cache-sticky-disk != 'true') || github.event_name != 'pull_request') && '1' || '0' }}",
|
||||
);
|
||||
expect(configureStep.run).toContain("OPENCLAW_VITEST_FS_MODULE_CACHE_WRITER=");
|
||||
// Prune work on a read-only sticky PR mount is discarded with the clone.
|
||||
expect(configureStep.env.CACHE_WRITER).toContain("github.event_name != 'pull_request'");
|
||||
@@ -2315,23 +2339,36 @@ describe("ci workflow guards", () => {
|
||||
const warmerSetup = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Setup Node environment",
|
||||
);
|
||||
const seedStep = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Select broad cache seed",
|
||||
);
|
||||
const warmStep = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Warm transform and compile caches",
|
||||
);
|
||||
|
||||
expect(warmer.concurrency["cancel-in-progress"]).toBe(false);
|
||||
expect(warmer.concurrency.group).toBe("vitest-cache-warm");
|
||||
expect(warmer.on.workflow_dispatch).toBeUndefined();
|
||||
expect(warmer.on.repository_dispatch.types).toEqual(["vitest-cache-warm"]);
|
||||
expect(warmer.jobs.warm.if).toBe("github.repository == 'openclaw/openclaw'");
|
||||
expect(warmer.jobs.warm.if).toContain("github.repository == 'openclaw/openclaw'");
|
||||
expect(warmerSource).toContain('cron: "17 8 * * *"');
|
||||
expect(warmerSource).toContain('candidate.shardName.startsWith("core-unit-fast")');
|
||||
expect(warmerSetup.with).toMatchObject({
|
||||
"node-compile-cache-scope": "test",
|
||||
"save-node-compile-cache": "true",
|
||||
"save-vitest-fs-cache": "true",
|
||||
"save-sticky-disk": "true",
|
||||
"sticky-disk": "true",
|
||||
"vitest-fs-cache": "true",
|
||||
});
|
||||
// The per-PR cache layer is gone, so its close-time cleanup workflow must
|
||||
// stay deleted; GitHub's own LRU/TTL eviction handles PR-ref archives.
|
||||
expect(existsSync(".github/workflows/pr-cache-cleanup.yml")).toBe(false);
|
||||
expect(warmerSetup.with["save-node-compile-cache"]).toContain(
|
||||
"github.event_name != 'workflow_run'",
|
||||
);
|
||||
expect(warmerSetup.with["save-vitest-fs-cache"]).toContain(
|
||||
"github.event_name != 'workflow_run'",
|
||||
);
|
||||
expect(seedStep.if).toBe("github.event_name != 'workflow_run'");
|
||||
expect(warmStep.if).toBe("github.event_name != 'workflow_run'");
|
||||
});
|
||||
|
||||
it("uses bundled Node shards and telemetry-backed runner sizes", () => {
|
||||
@@ -2468,9 +2505,9 @@ describe("ci workflow guards", () => {
|
||||
expect(pointStep.run).toContain('rm -rf "$sticky_root/gradle-user-home"');
|
||||
});
|
||||
|
||||
it("never keys a Blacksmith sticky disk by PR number or content hash", () => {
|
||||
// Blacksmith caps backing disks per installation; per-PR or per-hash key
|
||||
// segments mint unbounded disks until every sticky mount 429s fleet-wide.
|
||||
it("never keys a Blacksmith sticky disk by unbounded run dimensions", () => {
|
||||
// Blacksmith caps backing disks per installation; per-PR, per-commit,
|
||||
// per-run, or per-hash key segments mint disks until every mount 429s.
|
||||
// Snapshot validity belongs in in-job fingerprints/markers, never the key.
|
||||
const workflowFiles = readdirSync(".github/workflows")
|
||||
.filter((name) => name.endsWith(".yml"))
|
||||
@@ -2500,6 +2537,9 @@ describe("ci workflow guards", () => {
|
||||
expect(stickyKeys.length).toBeGreaterThan(0);
|
||||
for (const { file, key } of stickyKeys) {
|
||||
expect(key, file).not.toContain("github.event.pull_request.number");
|
||||
expect(key, file).not.toContain("github.sha");
|
||||
expect(key, file).not.toContain("github.ref");
|
||||
expect(key, file).not.toContain("github.run_");
|
||||
expect(key, file).not.toContain("hashFiles(");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user