mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
perf(ci): share trusted caches and fail fast on PR test failures (#3007)
* fix(ci): restore inherited validation failures * perf(ci): share trusted caches and fail fast on PR test failures * fix(ci): bound static cache and hash vitest config for knip - Bound the rolling .turbo/cache snapshot with 7d/500MB eviction so each trusted main save cannot accumulate history toward the GitHub limit. - Hash vitest.config.ts in knip inputs so removing the CiTestSequencer import invalidates the cached passing check. - Cover both in cache-policy and task-hash regressions.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
name: Cache npm package downloads
|
||||
description: Restore npm's download cache and save it only from trusted default-branch events.
|
||||
outputs:
|
||||
can-save:
|
||||
description: Whether this run may update the shared npm download cache.
|
||||
value: ${{ steps.context.outputs.can-save }}
|
||||
path:
|
||||
description: The npm download-cache directory.
|
||||
value: ${{ steps.context.outputs.path }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: context
|
||||
shell: bash
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
LOCKFILES_HASH: ${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json', '.npmrc') }}
|
||||
run: |
|
||||
cache_path="$(npm config get cache)"
|
||||
runtime_arch="$(node -p 'process.arch')"
|
||||
can_save=false
|
||||
if [[ -n "${DEFAULT_BRANCH}" && "${GITHUB_REF}" == "refs/heads/${DEFAULT_BRANCH}" && ( "${GITHUB_EVENT_NAME}" == "push" || "${GITHUB_EVENT_NAME}" == "schedule" || "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ) ]]; then
|
||||
can_save=true
|
||||
fi
|
||||
cache_key="npm-download-${RUNNER_OS}-${runtime_arch}-${LOCKFILES_HASH}"
|
||||
{
|
||||
printf 'can-save=%s\n' "${can_save}"
|
||||
printf 'path=%s\n' "${cache_path}"
|
||||
printf 'key=%s\n' "${cache_key}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
- name: Restore and save npm package downloads
|
||||
if: steps.context.outputs.can-save == 'true'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.context.outputs.path }}
|
||||
key: ${{ steps.context.outputs.key }}
|
||||
- name: Restore npm package downloads without saving
|
||||
if: steps.context.outputs.can-save != 'true'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.context.outputs.path }}
|
||||
key: ${{ steps.context.outputs.key }}
|
||||
+49
-15
@@ -122,8 +122,19 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Restore static task cache
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
id: static-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .turbo/cache/
|
||||
key: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-${{ github.sha }}
|
||||
# Turbo validates each task's inputs after restoring this snapshot.
|
||||
restore-keys: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-
|
||||
- name: Validate workflow syntax and expressions
|
||||
if: needs.changes.outputs.workflows == 'true'
|
||||
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -shellcheck= -pyflakes=
|
||||
@@ -136,6 +147,15 @@ jobs:
|
||||
run: npm run test:python
|
||||
- if: needs.changes.outputs.code != 'true'
|
||||
run: npm run format:check
|
||||
- name: Save validated static task cache
|
||||
if: >-
|
||||
success() && needs.changes.outputs.code == 'true' &&
|
||||
steps.npm-cache.outputs.can-save == 'true' &&
|
||||
steps.static-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .turbo/cache/
|
||||
key: ${{ steps.static-cache.outputs.cache-primary-key }}
|
||||
- name: Upload complete static diagnostics
|
||||
if: always() && needs.changes.outputs.code == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -153,7 +173,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Failed PRs release the remaining matrix slots; qualification retains
|
||||
# every shard's diagnostics and coverage. Passing PRs run the full suite.
|
||||
fail-fast: ${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }}
|
||||
matrix:
|
||||
shard: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6]
|
||||
steps:
|
||||
@@ -161,7 +183,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
@@ -212,7 +236,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Reuse portable build outputs across runs
|
||||
uses: ./.github/actions/portable-build-cache
|
||||
@@ -286,7 +312,10 @@ jobs:
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- if: needs.changes.outputs.code == 'true' && success()
|
||||
run: npm ci
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
@@ -450,7 +479,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- name: Verify native runtime identity
|
||||
env:
|
||||
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
|
||||
@@ -472,11 +503,6 @@ jobs:
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- name: Verify NativeAOT diagnostic applicability on this host
|
||||
run: npm run test:focused -- tests/evaluation/package/nativeAotDiagnostics.test.ts
|
||||
- name: Resolve npm cache before verifier HOME isolation
|
||||
id: npm-cache
|
||||
run: |
|
||||
cache_path="$(npm config get cache)"
|
||||
echo "path=${cache_path}" >> "${GITHUB_OUTPUT}"
|
||||
- run: npm run verify:package
|
||||
env:
|
||||
npm_config_cache: ${{ steps.npm-cache.outputs.path }}
|
||||
@@ -506,7 +532,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
@@ -554,7 +582,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Run the public CLI/MCP reconstruction journey
|
||||
env:
|
||||
@@ -583,7 +613,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
@@ -703,7 +735,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- run: npm run docs:check
|
||||
- name: Generation leaves authored files unchanged
|
||||
|
||||
@@ -23,6 +23,8 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- run: npm run docs:build
|
||||
|
||||
@@ -32,7 +32,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
|
||||
@@ -25,7 +25,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
|
||||
@@ -26,7 +26,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
|
||||
@@ -48,7 +48,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
|
||||
with:
|
||||
go-version: "1.26.1"
|
||||
|
||||
@@ -19,7 +19,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Pack CLI for setup verification
|
||||
shell: bash
|
||||
@@ -40,7 +42,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Install packed CLI in isolation
|
||||
shell: bash
|
||||
|
||||
@@ -19,7 +19,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- name: Install packed CLI in isolation
|
||||
shell: bash
|
||||
|
||||
@@ -27,7 +27,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
|
||||
@@ -47,7 +47,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y tshark util-linux python3
|
||||
- run: npm ci
|
||||
- run: npm run build:cached
|
||||
|
||||
@@ -29,7 +29,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
|
||||
@@ -40,7 +40,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: sudo apt-get update && sudo apt-get install -y sigrok-cli util-linux python3
|
||||
- run: npm ci
|
||||
- run: npm run build:cached
|
||||
|
||||
@@ -26,7 +26,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- name: Use pinned package manager
|
||||
run: npm install --global npm@11.16.0
|
||||
- run: npm ci
|
||||
|
||||
@@ -21,7 +21,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
|
||||
@@ -21,7 +21,9 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
|
||||
@@ -27,7 +27,9 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
check-latest: true
|
||||
cache: npm
|
||||
- name: Cache npm package downloads
|
||||
id: npm-cache
|
||||
uses: ./.github/actions/npm-cache
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
|
||||
@@ -141,6 +141,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
|
||||
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) |
|
||||
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
|
||||
| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
|
||||
| Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) |
|
||||
| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
|
||||
| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
|
||||
|
||||
+28
-5
@@ -887,8 +887,13 @@ successful ancestor push run of this CI workflow, rather than the preceding
|
||||
push: changes from failed or cancelled intermediate runs remain in scope.
|
||||
Missing, untrusted, or non-ancestor history selects every lane. The lookup is
|
||||
bounded to the last 100 successful push runs; lookup failures broaden selection.
|
||||
Every code change still runs all six portable shards, without coverage
|
||||
instrumentation in this fast tier. This deliberately avoids relying on an
|
||||
Every code change still selects all six portable shards, without coverage
|
||||
instrumentation in this fast tier. Ordinary PR matrices cancel remaining
|
||||
shards after the first failure; passing PRs execute the complete suite. Main,
|
||||
`ci:full`, scheduled, manual, and release qualification retain every shard's
|
||||
results after a failure. Available failed-shard reports remain diagnostic only:
|
||||
failed, cancelled, skipped, or missing selected shards cannot pass the gate or
|
||||
publish successful timing history. This deliberately avoids relying on an
|
||||
import graph for generated files, filesystem inputs, subprocesses, or bridges.
|
||||
|
||||
Scheduled qualification is admitted hourly at minute 23. A forced full run at
|
||||
@@ -916,10 +921,11 @@ classification job records its selection and broad-fallback reasons in the run
|
||||
summary. Representative Git-diff cases live in
|
||||
`tests/boundary/filesystem/ciChangeScope.test.ts`.
|
||||
|
||||
`CI required` always runs and checks classification and every selected lane,
|
||||
`CI required` checks classification and every selected lane,
|
||||
rejecting failed, cancelled, or unexpectedly skipped work. It rejects missing or
|
||||
unknown tiers and requires every canonical scope to be explicitly `true` or
|
||||
`false`; incomplete classification cannot silently omit verification lanes.
|
||||
The aggregate remains cancellable so superseded runs release their slots.
|
||||
Branch protection
|
||||
should require this aggregate, `Static checks`, `Portable tests`, and
|
||||
`Conventional PR title`. Title validation runs separately on PR opening,
|
||||
@@ -968,7 +974,7 @@ filename cases.
|
||||
|
||||
POSIX package verification resolves the runner's configured npm cache before
|
||||
creating disposable client homes and passes that location to its child installs.
|
||||
This reuses the download cache restored by `setup-node` while keeping client
|
||||
This reuses the download cache restored by the shared npm-cache action while keeping client
|
||||
configuration isolated. Installs retain their existing registry and integrity
|
||||
checks.
|
||||
|
||||
@@ -998,7 +1004,24 @@ thresholds remain enforced in full qualification.
|
||||
Missing or unusable history falls back to Vitest's default assignment. Only
|
||||
timing data is reused; live provider results are never cached. Turbo outputs are
|
||||
restored by task hash, with trusted default-branch runs as cache writers and PRs
|
||||
as restore-only consumers. npm caches hold downloads, not `node_modules`.
|
||||
as restore-only consumers. The static job also restores its Turbo snapshot;
|
||||
each task revalidates its own input hash before reusing outputs or diagnostics.
|
||||
The Node pin and npm install settings are global hash inputs. Knip includes its ignore rules and VitePress
|
||||
entry, so changes to those inputs cannot reuse an older successful check.
|
||||
|
||||
The shared npm-cache action caches downloads, not `node_modules`, and every
|
||||
consumer still runs `npm ci`. Keys cover the runner OS, actual Node architecture,
|
||||
root lockfiles, and `.npmrc`. Only push, schedule, and manual events on the
|
||||
repository's default branch may save shared npm caches; PRs restore only.
|
||||
Static snapshots use the same write authority and are saved only after the
|
||||
static job's validation succeeds. Main warms the new cache namespace after
|
||||
merge; a cold cache or changed lockfile falls back to the normal install.
|
||||
This prevents each PR from saving another branch-scoped copy of the same
|
||||
download cache. It does not remove runner assignment waits.
|
||||
|
||||
Portable owner-based test selection remains observational. Its reports compare
|
||||
proposed omissions with the full run; incomplete or skipped observations do not
|
||||
establish that fewer tests would preserve regression detection.
|
||||
|
||||
The eight automatic browser, binary-layout, EVM, JavaScript-recovery,
|
||||
recorded-crash, and web provider lanes download `rea-build-cache` from their own
|
||||
|
||||
@@ -331,7 +331,9 @@ describe("managed operand extent diagnostics", () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("managed complete operand decoding", () => {
|
||||
// The extent check must also reject nothing that fits, so every multi-byte
|
||||
// width is exercised at exactly its own size with a known value. Each body
|
||||
// ends at its operand, so a width declared too large turns a valid operand
|
||||
|
||||
@@ -443,7 +443,9 @@ describe("artifact comparison completeness", () => {
|
||||
changes: [],
|
||||
});
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
describe("artifact comparison fragment integrity", () => {
|
||||
it("assembles real inventory fragments and retains every citation", async () => {
|
||||
const parent = await createTestTempDirectory("rea-artifact-fragments-");
|
||||
const leftPath = join(parent, "left.app");
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { chmod, readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { delimiter, join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const repository = process.cwd();
|
||||
const actionPath = join(repository, ".github/actions/npm-cache/action.yml");
|
||||
const readAction = async () =>
|
||||
z
|
||||
.object({
|
||||
outputs: z.record(z.string(), z.object({ value: z.string() })),
|
||||
runs: z.object({
|
||||
steps: z.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
if: z.string().optional(),
|
||||
uses: z.string().optional(),
|
||||
with: z.record(z.string(), z.string()).optional(),
|
||||
run: z.string().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
})
|
||||
.parse(parse(await readFile(actionPath, "utf8")));
|
||||
|
||||
const readWorkflows = async () => {
|
||||
const directory = join(repository, ".github/workflows");
|
||||
const files = (await readdir(directory)).filter((file) =>
|
||||
/\.ya?ml$/u.test(file),
|
||||
);
|
||||
return Promise.all(
|
||||
files.map(async (file) => ({
|
||||
file,
|
||||
workflow: z
|
||||
.object({
|
||||
jobs: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
steps: z.array(z.record(z.string(), z.unknown())).optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.parse(parse(await readFile(join(directory, file), "utf8"))),
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
describe("hosted npm download-cache policy", () => {
|
||||
it("only allows shared writes for trusted original default-branch events", async () => {
|
||||
const action = await readAction();
|
||||
const policy = action.runs.steps.find((step) => step.id === "context");
|
||||
const command = z.string().parse(policy?.run);
|
||||
|
||||
const outputDirectory = await createTestTempDirectory(
|
||||
"rea-ci-cache-policy-",
|
||||
);
|
||||
const npmStub = join(outputDirectory, "npm");
|
||||
await writeFile(
|
||||
npmStub,
|
||||
String.raw`#!/bin/sh
|
||||
printf '%s\n' 'C:\Users\runner with spaces\AppData\Local\npm-cache'
|
||||
`,
|
||||
);
|
||||
await chmod(npmStub, 0o755);
|
||||
|
||||
for (const [event, ref, defaultBranch, expected] of [
|
||||
["push", "refs/heads/main", "main", "true"],
|
||||
["schedule", "refs/heads/main", "main", "true"],
|
||||
["workflow_dispatch", "refs/heads/main", "main", "true"],
|
||||
["push", "refs/heads/stable", "stable", "true"],
|
||||
["pull_request", "refs/heads/main", "main", "false"],
|
||||
["pull_request_target", "refs/heads/main", "main", "false"],
|
||||
["workflow_call", "refs/heads/main", "main", "false"],
|
||||
["push", "refs/heads/feature/cache", "main", "false"],
|
||||
["push", "refs/heads/main", "stable", "false"],
|
||||
["push", "refs/heads/main", "", "false"],
|
||||
]) {
|
||||
const outputPath = join(
|
||||
outputDirectory,
|
||||
`policy-${Date.now()}-${Math.random()}`,
|
||||
);
|
||||
execFileSync("bash", ["-euo", "pipefail", "-c", command], {
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${outputDirectory}${delimiter}${process.env.PATH ?? ""}`,
|
||||
GITHUB_EVENT_NAME: event,
|
||||
GITHUB_REF: ref,
|
||||
GITHUB_OUTPUT: outputPath,
|
||||
DEFAULT_BRANCH: defaultBranch,
|
||||
LOCKFILES_HASH: "lock-hash",
|
||||
RUNNER_OS: "Windows",
|
||||
},
|
||||
});
|
||||
const output = await readFile(outputPath, "utf8");
|
||||
expect(output).toContain(`can-save=${expected}\n`);
|
||||
expect(output).toContain(
|
||||
`path=${String.raw`C:\Users\runner with spaces\AppData\Local\npm-cache`}\n`,
|
||||
);
|
||||
expect(output).toContain(
|
||||
`key=npm-download-Windows-${process.arch}-lock-hash\n`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a lockfile-, npmrc-, OS-, and runtime-architecture-scoped download cache", async () => {
|
||||
const action = await readAction();
|
||||
const policy = action.runs.steps.find((step) => step.id === "context");
|
||||
const restoreAndSave = action.runs.steps.find((step) =>
|
||||
step.uses?.startsWith("actions/cache@"),
|
||||
);
|
||||
const restoreOnly = action.runs.steps.find((step) =>
|
||||
step.uses?.startsWith("actions/cache/restore@"),
|
||||
);
|
||||
|
||||
expect(action.outputs["can-save"]?.value).toBe(
|
||||
"${{ steps.context.outputs.can-save }}",
|
||||
);
|
||||
expect(action.outputs.path?.value).toBe(
|
||||
"${{ steps.context.outputs.path }}",
|
||||
);
|
||||
expect(policy?.env?.DEFAULT_BRANCH).toBe(
|
||||
"${{ github.event.repository.default_branch }}",
|
||||
);
|
||||
expect(policy?.env?.LOCKFILES_HASH).toBe(
|
||||
"${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json', '.npmrc') }}",
|
||||
);
|
||||
expect(policy?.run).toContain(
|
||||
'cache_key="npm-download-${RUNNER_OS}-${runtime_arch}-${LOCKFILES_HASH}"',
|
||||
);
|
||||
expect(restoreAndSave?.uses).toBe(
|
||||
"actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830",
|
||||
);
|
||||
expect(restoreAndSave?.if).toBe("steps.context.outputs.can-save == 'true'");
|
||||
expect(restoreOnly?.uses).toBe(
|
||||
"actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830",
|
||||
);
|
||||
expect(restoreOnly?.if).toBe("steps.context.outputs.can-save != 'true'");
|
||||
expect(restoreAndSave?.with?.path).toBe(
|
||||
"${{ steps.context.outputs.path }}",
|
||||
);
|
||||
expect(restoreOnly?.with?.path).toBe(restoreAndSave?.with?.path);
|
||||
expect(restoreAndSave?.with?.key).toBe("${{ steps.context.outputs.key }}");
|
||||
expect(restoreOnly?.with?.key).toBe(restoreAndSave?.with?.key);
|
||||
expect(restoreAndSave?.with).not.toHaveProperty("restore-keys");
|
||||
expect(restoreOnly?.with).not.toHaveProperty("restore-keys");
|
||||
});
|
||||
|
||||
it("replaces every inline setup-node npm cache immediately and preserves conditions", async () => {
|
||||
const workflows = await readWorkflows();
|
||||
let npmCacheSteps = 0;
|
||||
|
||||
for (const { file, workflow } of workflows) {
|
||||
const source = await readFile(
|
||||
join(repository, ".github/workflows", file),
|
||||
"utf8",
|
||||
);
|
||||
expect(source).not.toMatch(/^\s+cache:\s*npm\s*$/mu);
|
||||
for (const [jobName, job] of Object.entries(workflow.jobs ?? {})) {
|
||||
const steps = job.steps ?? [];
|
||||
for (let index = 0; index < steps.length; index++) {
|
||||
const step = steps[index];
|
||||
if (step?.uses !== "./.github/actions/npm-cache") continue;
|
||||
npmCacheSteps++;
|
||||
expect(step.id).toBe("npm-cache");
|
||||
const setup = steps[index - 1];
|
||||
expect(
|
||||
typeof setup?.uses === "string" &&
|
||||
setup.uses.startsWith("actions/setup-node@"),
|
||||
`${file}:${jobName} npm cache action must immediately follow setup-node`,
|
||||
).toBe(true);
|
||||
expect(step.if).toBe(setup?.if);
|
||||
expect(setup?.with).not.toHaveProperty("cache", "npm");
|
||||
const npmCiIndex = steps.findIndex(
|
||||
(candidate, candidateIndex) =>
|
||||
candidateIndex > index && candidate.run === "npm ci",
|
||||
);
|
||||
expect(
|
||||
npmCiIndex,
|
||||
`${file}:${jobName} retains npm ci after cache`,
|
||||
).toBeGreaterThan(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(npmCacheSteps).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("shares static task snapshots only after validation using the npm cache owner's write authority", async () => {
|
||||
const workflows = await readWorkflows();
|
||||
const ci = workflows.find(({ file }) => file === "ci.yml");
|
||||
const steps = ci?.workflow.jobs?.static?.steps ?? [];
|
||||
const restore = steps.find((step) => step.id === "static-cache");
|
||||
const save = steps.find(
|
||||
(step) => step.name === "Save validated static task cache",
|
||||
);
|
||||
expect(restore?.uses).toBe(
|
||||
"actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830",
|
||||
);
|
||||
expect(restore?.if).toBe("needs.changes.outputs.code == 'true'");
|
||||
expect(restore?.with).toMatchObject({ path: ".turbo/cache/" });
|
||||
expect(save?.uses).toBe(
|
||||
"actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830",
|
||||
);
|
||||
expect(save?.if).toContain("success()");
|
||||
expect(save?.if).toContain("steps.npm-cache.outputs.can-save == 'true'");
|
||||
expect(save?.with).toEqual({
|
||||
path: ".turbo/cache/",
|
||||
key: "${{ steps.static-cache.outputs.cache-primary-key }}",
|
||||
});
|
||||
expect(steps.indexOf(save!)).toBeGreaterThan(
|
||||
steps.findIndex((step) => step.run === "npm run test:python"),
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds the rolling static snapshot with Turbo age and size eviction", async () => {
|
||||
const turbo = z
|
||||
.object({
|
||||
cacheMaxAge: z.string(),
|
||||
cacheMaxSize: z.string(),
|
||||
tasks: z.record(
|
||||
z.string(),
|
||||
z.object({ inputs: z.array(z.unknown()).optional() }),
|
||||
),
|
||||
})
|
||||
.parse(JSON.parse(await readFile(join(repository, "turbo.json"), "utf8")));
|
||||
// The static job restores the previous snapshot by prefix and saves the
|
||||
// whole .turbo/cache directory again. Without local eviction every main
|
||||
// commit carries all historical entries into a new immutable GitHub cache,
|
||||
// growing transfers toward the 10 GB repository limit.
|
||||
expect(turbo.cacheMaxAge).toMatch(/^[1-9]\d*(s|m|h|d|w)$/u);
|
||||
expect(turbo.cacheMaxAge).not.toBe("0");
|
||||
const size = /^(\d+(?:\.\d+)?)(MB|GB|TB)$/iu.exec(turbo.cacheMaxSize);
|
||||
expect(size, "cacheMaxSize must use an explicit MB/GB/TB budget").not.toBe(
|
||||
null,
|
||||
);
|
||||
const value = Number(size?.[1] ?? "0");
|
||||
const unit = (size?.[2] ?? "MB").toUpperCase();
|
||||
const megabytes =
|
||||
unit === "TB" ? value * 1024 * 1024 : unit === "GB" ? value * 1024 : value;
|
||||
expect(megabytes).toBeGreaterThan(0);
|
||||
expect(megabytes).toBeLessThanOrEqual(1024);
|
||||
expect(turbo.tasks["knip"]?.inputs).toContain("vitest.config.ts");
|
||||
});
|
||||
@@ -506,3 +506,44 @@ it("rejects incomplete or malformed classification even when available jobs succ
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("fails fast only for ordinary PR matrices and rejects partial portable feedback", async () => {
|
||||
const { jobs } = await workflow();
|
||||
expect(jobs["test-shard"]?.strategy).toMatchObject({
|
||||
"fail-fast":
|
||||
"${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }}",
|
||||
});
|
||||
expect(jobs["package-e2e"]?.strategy).toMatchObject({ "fail-fast": false });
|
||||
const flags = await scopeFlags();
|
||||
for (const result of ["failure", "cancelled", "skipped", "missing"]) {
|
||||
// Even an incorrectly successful report merge cannot hide matrix failure.
|
||||
const needs: Record<string, unknown> = {
|
||||
changes: {
|
||||
result: "success",
|
||||
outputs: {
|
||||
...Object.fromEntries(flags.map((flag) => [flag, "false"])),
|
||||
tier: "fast",
|
||||
code: "true",
|
||||
},
|
||||
},
|
||||
static: { result: "success" },
|
||||
build: { result: "success" },
|
||||
test: { result: "success" },
|
||||
...(result === "missing" ? {} : { "test-shard": { result } }),
|
||||
};
|
||||
await expect(
|
||||
executeWorkflowFixture(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(
|
||||
new URL("../../../scripts/ci/required.mjs", import.meta.url),
|
||||
),
|
||||
],
|
||||
{ env: { ...process.env, CI_JOBS: JSON.stringify(needs) } },
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
code: 1,
|
||||
stderr: expect.stringContaining("test-shard"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { workflowGit } from "../../support/workflowGit.js";
|
||||
|
||||
const repository = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const turbo = join(repository, "node_modules/turbo/bin/turbo");
|
||||
const reportScript = join(repository, "scripts/ci/static-check-report.mjs");
|
||||
const rootConfigSchema = z.object({
|
||||
globalDependencies: z.array(z.string()).optional(),
|
||||
globalPassThroughEnv: z.array(z.string()).optional(),
|
||||
tasks: z.record(z.string(), z.record(z.string(), z.unknown())),
|
||||
});
|
||||
const dryRunSchema = z.object({
|
||||
tasks: z.array(z.object({ taskId: z.string(), hash: z.string() })),
|
||||
});
|
||||
|
||||
const execute = (command: string, args: string[], cwd: string) =>
|
||||
spawnSync(process.execPath, [command, ...args], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
timeout: 30_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
const readRootConfig = async () =>
|
||||
rootConfigSchema.parse(
|
||||
JSON.parse(await readFile(join(repository, "turbo.json"), "utf8")),
|
||||
);
|
||||
|
||||
const writeTaskFixture = async (
|
||||
root: string,
|
||||
taskNames: string[],
|
||||
scripts: Record<string, string>,
|
||||
) => {
|
||||
const config = await readRootConfig();
|
||||
const tasks = Object.fromEntries(
|
||||
taskNames.map((name) => {
|
||||
const task = config.tasks[name];
|
||||
if (task === undefined) throw new Error(`Missing Turbo task ${name}`);
|
||||
// These tests measure a task's own inputs. Drop dependency edges so the
|
||||
// unrelated compile task cannot add fixture-specific inputs or outputs.
|
||||
const { dependsOn: _dependsOn, ...definition } = task;
|
||||
return [name, definition];
|
||||
}),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "turbo.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
...(config.globalDependencies === undefined
|
||||
? {}
|
||||
: { globalDependencies: config.globalDependencies }),
|
||||
...(config.globalPassThroughEnv === undefined
|
||||
? {}
|
||||
: { globalPassThroughEnv: config.globalPassThroughEnv }),
|
||||
tasks,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "rea-static-task-cache-fixture",
|
||||
version: "1.0.0",
|
||||
private: true,
|
||||
packageManager: "npm@11.16.0",
|
||||
scripts,
|
||||
}),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "package-lock.json"),
|
||||
JSON.stringify({
|
||||
name: "rea-static-task-cache-fixture",
|
||||
version: "1.0.0",
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": { name: "rea-static-task-cache-fixture", version: "1.0.0" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const runTurbo = (
|
||||
root: string,
|
||||
task: string,
|
||||
cacheDirectory: string,
|
||||
dryRun = false,
|
||||
) =>
|
||||
execute(
|
||||
turbo,
|
||||
[
|
||||
"run",
|
||||
task,
|
||||
...(dryRun ? ["--dry=json"] : ["--output-logs=errors-only"]),
|
||||
"--cache=local:rw,remote:r",
|
||||
"--cache-dir",
|
||||
cacheDirectory,
|
||||
],
|
||||
root,
|
||||
);
|
||||
|
||||
const taskHash = (root: string, task: string, cacheDirectory: string) => {
|
||||
const result = runTurbo(root, task, cacheDirectory, true);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
const { tasks } = dryRunSchema.parse(JSON.parse(result.stdout));
|
||||
const entry = tasks.find(({ taskId }) => taskId === task);
|
||||
if (entry === undefined) throw new Error(`Turbo omitted task ${task}`);
|
||||
return entry.hash;
|
||||
};
|
||||
|
||||
it("restores a real lint report across cold, warm, and fresh consumer checkouts", async () => {
|
||||
const producer = await createTestTempDirectory("rea-static-cache-producer-");
|
||||
const consumer = await createTestTempDirectory("rea-static-cache-consumer-");
|
||||
const cache = await createTestTempDirectory("rea-static-cache-store-");
|
||||
const script = "node scripts/ci/static-check-report.mjs lint";
|
||||
for (const root of [producer, consumer]) {
|
||||
await mkdir(join(root, "scripts/ci"), { recursive: true });
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await symlink(
|
||||
join(repository, "node_modules"),
|
||||
join(root, "node_modules"),
|
||||
"junction",
|
||||
);
|
||||
await writeTaskFixture(root, ["lint"], { lint: script });
|
||||
await writeFile(
|
||||
join(root, ".nvmrc"),
|
||||
await readFile(join(repository, ".nvmrc")),
|
||||
);
|
||||
await writeFile(join(root, ".gitignore"), ".cache/\nnode_modules/\n");
|
||||
await writeFile(
|
||||
join(root, ".oxlintrc.json"),
|
||||
JSON.stringify({ rules: { "no-debugger": "error" } }),
|
||||
);
|
||||
await writeFile(join(root, "src/check.js"), "export const ready = true;\n");
|
||||
await writeFile(
|
||||
join(root, "scripts/ci/static-check-report.mjs"),
|
||||
await readFile(reportScript, "utf8"),
|
||||
);
|
||||
await workflowGit(root, ["init", "--initial-branch=main"]);
|
||||
await workflowGit(root, ["add", "."]);
|
||||
await workflowGit(root, ["commit", "-m", "test: seed static task cache"]);
|
||||
}
|
||||
|
||||
const cold = runTurbo(producer, "lint", cache);
|
||||
expect(cold.error).toBeUndefined();
|
||||
expect(cold.status, cold.stderr).toBe(0);
|
||||
expect(cold.stdout).toMatch(/Cached:\s+0 cached/u);
|
||||
const report = join(producer, ".cache/static-checks/oxlint.json");
|
||||
const cleanReport = await readFile(report, "utf8");
|
||||
|
||||
await rm(join(producer, ".cache"), { recursive: true });
|
||||
const warm = runTurbo(producer, "lint", cache);
|
||||
expect(warm.error).toBeUndefined();
|
||||
expect(warm.status, warm.stderr).toBe(0);
|
||||
expect(warm.stdout).toMatch(/Cached:\s+1 cached/u);
|
||||
expect(await readFile(report, "utf8")).toBe(cleanReport);
|
||||
|
||||
const freshConsumer = runTurbo(consumer, "lint", cache);
|
||||
expect(freshConsumer.error).toBeUndefined();
|
||||
expect(freshConsumer.status, freshConsumer.stderr).toBe(0);
|
||||
expect(freshConsumer.stdout).toMatch(/Cached:\s+1 cached/u);
|
||||
expect(
|
||||
await readFile(join(consumer, ".cache/static-checks/oxlint.json"), "utf8"),
|
||||
).toBe(cleanReport);
|
||||
|
||||
await writeFile(join(consumer, "src/check.js"), "debugger;\n");
|
||||
const invalid = runTurbo(consumer, "lint", cache);
|
||||
expect(invalid.error).toBeUndefined();
|
||||
expect(invalid.status).not.toBe(0);
|
||||
expect(invalid.stdout).toMatch(/Cached:\s+0 cached/u);
|
||||
const { diagnostics } = z
|
||||
.object({
|
||||
diagnostics: z.array(
|
||||
z.object({ code: z.string().optional(), severity: z.string() }),
|
||||
),
|
||||
})
|
||||
.parse(
|
||||
JSON.parse(
|
||||
await readFile(
|
||||
join(consumer, ".cache/static-checks/oxlint.json"),
|
||||
"utf8",
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(diagnostics).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "eslint(no-debugger)",
|
||||
severity: "error",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("invalidates checker task hashes for Node, ignore, and VitePress config changes", async () => {
|
||||
const root = await createTestTempDirectory("rea-static-cache-inputs-");
|
||||
const cache = await createTestTempDirectory("rea-static-cache-hashes-");
|
||||
await mkdir(join(root, "docs/.vitepress"), { recursive: true });
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await mkdir(join(root, "tests"), { recursive: true });
|
||||
await writeTaskFixture(
|
||||
root,
|
||||
["jscpd", "knip", "metadata:check", "scan:todos"],
|
||||
// This fixture probes only Turbo's real task hash; the checker commands
|
||||
// are intentionally inert so the hash test cannot invoke large suites.
|
||||
{
|
||||
jscpd: 'node -e "process.exit(0)"',
|
||||
knip: 'node -e "process.exit(0)"',
|
||||
"metadata:check": 'node -e "process.exit(0)"',
|
||||
"scan:todos": 'node -e "process.exit(0)"',
|
||||
},
|
||||
);
|
||||
await writeFile(join(root, ".nvmrc"), "24.18.0\n");
|
||||
await writeFile(join(root, ".npmrc"), "engine-strict=true\n");
|
||||
await writeFile(join(root, ".gitignore"), ".cache/\n");
|
||||
await writeFile(join(root, ".jscpd.json"), "{}\n");
|
||||
await writeFile(join(root, "docs/.vitepress/config.ts"), "export {};\n");
|
||||
await writeFile(
|
||||
join(root, "vitest.config.ts"),
|
||||
'import { CiTestSequencer } from "./tests/ciTestSequencer.js";\nexport const sequencer = CiTestSequencer;\n',
|
||||
);
|
||||
await writeFile(join(root, "src/input.ts"), "export {};\n");
|
||||
await writeFile(join(root, "tests/input.ts"), "export {};\n");
|
||||
|
||||
const initial = new Map<string, string>();
|
||||
for (const task of ["jscpd", "knip", "metadata:check", "scan:todos"])
|
||||
initial.set(task, taskHash(root, task, cache));
|
||||
|
||||
await writeFile(join(root, ".nvmrc"), "24.19.0\n");
|
||||
for (const task of ["jscpd", "metadata:check", "scan:todos"])
|
||||
expect(
|
||||
taskHash(root, task, cache),
|
||||
`${task} must hash the Node pin`,
|
||||
).not.toBe(initial.get(task));
|
||||
await writeFile(join(root, ".nvmrc"), "24.18.0\n");
|
||||
|
||||
await writeFile(join(root, ".npmrc"), "engine-strict=false\n");
|
||||
for (const task of ["jscpd", "knip", "metadata:check", "scan:todos"])
|
||||
expect(
|
||||
taskHash(root, task, cache),
|
||||
`${task} must hash npm settings`,
|
||||
).not.toBe(initial.get(task));
|
||||
await writeFile(join(root, ".npmrc"), "engine-strict=true\n");
|
||||
for (const task of ["jscpd", "metadata:check", "scan:todos"])
|
||||
expect(taskHash(root, task, cache), `${task} restores after Node pin`).toBe(
|
||||
initial.get(task),
|
||||
);
|
||||
|
||||
await writeFile(join(root, ".gitignore"), ".cache/\ncoverage/\n");
|
||||
for (const task of ["jscpd", "knip"])
|
||||
expect(taskHash(root, task, cache), `${task} must hash gitignore`).not.toBe(
|
||||
initial.get(task),
|
||||
);
|
||||
await writeFile(join(root, ".gitignore"), ".cache/\n");
|
||||
for (const task of ["jscpd", "knip"])
|
||||
expect(
|
||||
taskHash(root, task, cache),
|
||||
`${task} restores after gitignore`,
|
||||
).toBe(initial.get(task));
|
||||
|
||||
await writeFile(
|
||||
join(root, "docs/.vitepress/config.ts"),
|
||||
"export const configRevision = 2;\n",
|
||||
);
|
||||
expect(
|
||||
taskHash(root, "knip", cache),
|
||||
"knip must hash its configured VitePress entry",
|
||||
).not.toBe(initial.get("knip"));
|
||||
await writeFile(join(root, "docs/.vitepress/config.ts"), "export {};\n");
|
||||
|
||||
await writeFile(join(root, "vitest.config.ts"), "export {};\n");
|
||||
expect(
|
||||
taskHash(root, "knip", cache),
|
||||
"knip must hash vitest.config.ts so unused sequencer imports invalidate the cache",
|
||||
).not.toBe(initial.get("knip"));
|
||||
for (const task of ["jscpd", "metadata:check", "scan:todos"])
|
||||
expect(
|
||||
taskHash(root, task, cache),
|
||||
`${task} must ignore vitest.config.ts`,
|
||||
).toBe(initial.get(task));
|
||||
});
|
||||
@@ -343,9 +343,7 @@ describe("overview document identity through MCP", () => {
|
||||
details: {
|
||||
provider_id: "fixture",
|
||||
operation: "list_documents",
|
||||
reason: expect.stringContaining(
|
||||
"provider lacks document selection",
|
||||
),
|
||||
reason: expect.stringContaining("provider lacks document selection"),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,10 @@ const execution = (failure: unknown, status = 1) => ({
|
||||
stdout: JSON.stringify(failure),
|
||||
});
|
||||
|
||||
const overview = (procedure_count?: unknown, operation = "binary_overview") => ({
|
||||
const overview = (
|
||||
procedure_count?: unknown,
|
||||
operation = "binary_overview",
|
||||
) => ({
|
||||
operation,
|
||||
...(procedure_count === undefined
|
||||
? {}
|
||||
|
||||
+7
-14
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.json",
|
||||
"cacheMaxAge": "7d",
|
||||
"cacheMaxSize": "500MB",
|
||||
"globalDependencies": [".nvmrc", ".npmrc"],
|
||||
"globalPassThroughEnv": ["GOMAXPROCS", "GOMEMLIMIT", "UV_THREADPOOL_SIZE"],
|
||||
"tasks": {
|
||||
"compile": {
|
||||
"dependsOn": ["metadata:check"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/check-dependency-install.mjs",
|
||||
@@ -22,7 +24,6 @@
|
||||
},
|
||||
"typecheck": {
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"src/**/*.ts",
|
||||
@@ -49,7 +50,6 @@
|
||||
"outputs": [".cache/static-checks/oxlint.json"],
|
||||
"inputs": [
|
||||
".gitignore",
|
||||
".nvmrc",
|
||||
".oxlintrc.json",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
@@ -71,7 +71,6 @@
|
||||
"outputs": [".cache/static-checks/oxlint.json"],
|
||||
"inputs": [
|
||||
".gitignore",
|
||||
".nvmrc",
|
||||
".oxlintrc.json",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
@@ -92,7 +91,7 @@
|
||||
"knip": {
|
||||
"dependsOn": ["compile"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
".gitignore",
|
||||
"knip.json",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
@@ -100,7 +99,9 @@
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts",
|
||||
"tsconfig.json",
|
||||
"tsconfig.build.json"
|
||||
"tsconfig.build.json",
|
||||
"vitest.config.ts",
|
||||
"docs/.vitepress/config.ts"
|
||||
]
|
||||
},
|
||||
"jscpd": {
|
||||
@@ -139,7 +140,6 @@
|
||||
"artifacts:skills"
|
||||
],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-*.mjs",
|
||||
@@ -171,7 +171,6 @@
|
||||
"artifacts:mcp-catalog": {
|
||||
"dependsOn": ["compile"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-mcp-tool-catalog.mjs",
|
||||
@@ -182,7 +181,6 @@
|
||||
"artifacts:skills": {
|
||||
"dependsOn": ["compile"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-skill-metadata.mjs",
|
||||
@@ -194,7 +192,6 @@
|
||||
"artifacts:product-catalog": {
|
||||
"dependsOn": ["compile"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-product-catalog.mjs",
|
||||
@@ -206,7 +203,6 @@
|
||||
"artifacts:managed-evidence": {
|
||||
"dependsOn": ["artifacts:skills"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-completion-ledger.mjs",
|
||||
@@ -222,7 +218,6 @@
|
||||
"artifacts:error-schema": {
|
||||
"dependsOn": ["compile"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-error-schema.mjs",
|
||||
@@ -235,7 +230,6 @@
|
||||
"mcp-catalog:check:built": {
|
||||
"dependsOn": ["artifacts:mcp-catalog"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-mcp-tool-catalog.mjs",
|
||||
@@ -249,7 +243,6 @@
|
||||
"evidence:check:built": {
|
||||
"dependsOn": ["artifacts:managed-evidence"],
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-completion-ledger.mjs",
|
||||
|
||||
Reference in New Issue
Block a user