Skip to content

Commit 3aa89ae

Browse files
authored
Merge branch 'main' into feat/task-title-summary
2 parents 1e4e3c2 + a0d03a0 commit 3aa89ae

9 files changed

Lines changed: 491 additions & 123 deletions

File tree

‎.github/workflows/code-qa.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ jobs:
247247
- name: Verify coverage cache inputs
248248
if: matrix.collect-coverage
249249
run: pnpm --dir src run verify:coverage-cache-inputs
250+
# Validate build/static cache inputs (issue #2290 contract) so a
251+
# stale webview or extension bundle is never restored as a hit.
252+
- name: Verify build cache inputs
253+
if: matrix.collect-coverage
254+
run: pnpm --dir src run verify:build-cache-inputs
250255
- name: Save Turbo cache
251256
if: steps.turbo-cache.outputs.cache-hit != 'true'
252257
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0

‎src/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@
454454
"test": "vitest run",
455455
"verify:coverage-contract": "node scripts/verify-coverage-contract.mjs",
456456
"verify:coverage-cache-inputs": "node --test scripts/verify-coverage-cache-inputs.mjs",
457+
"verify:build-cache-inputs": "node --test scripts/verify-cache-probe.mjs scripts/verify-build-cache-inputs.mjs",
457458
"merge:coverage": "node scripts/merge-lcov.mjs coverage/merged/lcov.info coverage/api/lcov.info coverage/core/lcov.info coverage/services/lcov.info coverage/misc/lcov.info coverage/tree-sitter/lcov.info",
458459
"test:unit": "vitest run --config vitest.unit.config.ts",
459460
"test:dist": "vitest run --config vitest.dist.config.ts",

‎src/scripts/cache-probe.mjs‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { spawnSync } from "node:child_process"
2+
import { mkdtempSync, rmSync } from "node:fs"
3+
import { tmpdir } from "node:os"
4+
import { resolve } from "node:path"
5+
import process from "node:process"
6+
7+
const root = resolve(import.meta.dirname, "../..")
8+
const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm"
9+
if (!pnpm) throw new Error("pnpm executable path is unavailable")
10+
const command = process.platform === "win32" ? process.execPath : pnpm
11+
const args = process.platform === "win32" ? [pnpm] : []
12+
13+
const gitError = (result) => {
14+
const details = [result.error?.message, result.signal, result.stderr, result.stdout].filter(Boolean).join("\n")
15+
return new Error(details || `git exited with status ${result.status ?? "unknown"}`)
16+
}
17+
18+
const git = (gitArgs) => {
19+
const result = spawnSync("git", gitArgs, { cwd: root, encoding: "utf8" })
20+
if (result.status !== 0) throw gitError(result)
21+
}
22+
23+
const trackedTreeStatus = () =>
24+
spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=no"], { cwd: root, encoding: "utf8" })
25+
26+
// Probe the working tree so local runs validate uncommitted task edits. CI
27+
// runs on a clean tree, where stash create exits without a SHA and this
28+
// falls back to HEAD. git stash create can exit 1 with empty output when
29+
// stale index metadata refreshes to a clean tracked tree. Confirm a clean
30+
// tracked tree before trusting that exit.
31+
export const stashCommit = (result, trackedTree = trackedTreeStatus) => {
32+
if (result.status === 0) return result.stdout.trim() || "HEAD"
33+
const quietExit = result.status === 1 && !result.error && !result.signal && !result.stdout && !result.stderr
34+
if (!quietExit) throw gitError(result)
35+
const status = trackedTree()
36+
if (status.status !== 0) throw gitError(status)
37+
if (status.stdout.trim() !== "") {
38+
const changes = status.stdout.trim()
39+
throw new Error(`git stash create found no changes, but git status reports tracked changes:\n${changes}`)
40+
}
41+
return "HEAD"
42+
}
43+
44+
const worktreeCommit = () => stashCommit(spawnSync("git", ["stash", "create"], { cwd: root, encoding: "utf8" }))
45+
46+
export const turboGraph = (probeRoot, taskArgs, env = process.env) => {
47+
const result = spawnSync(
48+
command,
49+
[...args, "turbo", "--cwd", probeRoot, "run", ...taskArgs, "--dry=json", "--no-daemon"],
50+
{ cwd: root, encoding: "utf8", env },
51+
)
52+
if (result.status !== 0)
53+
throw new Error(
54+
[result.error?.message, result.signal, result.stderr, result.stdout].filter(Boolean).join("\n") ||
55+
`pnpm exited with status ${result.status ?? "unknown"}`,
56+
)
57+
const lines = result.stdout.split("\n")
58+
let jsonStart = -1
59+
for (let i = 0; i < lines.length; i++) {
60+
if (lines[i].startsWith("{")) jsonStart = i
61+
}
62+
if (jsonStart < 0) {
63+
const details = [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n")
64+
throw new Error(details || "turbo dry run produced no JSON graph")
65+
}
66+
return JSON.parse(lines.slice(jsonStart).join("\n"))
67+
}
68+
69+
export const createProbe = () => {
70+
const probeRoot = mkdtempSync(resolve(tmpdir(), "zoo-build-cache-probe-"))
71+
let worktreeAdded = false
72+
let cleaned = false
73+
const cleanup = () => {
74+
if (cleaned) return
75+
cleaned = true
76+
try {
77+
if (worktreeAdded) git(["worktree", "remove", "--force", probeRoot])
78+
} finally {
79+
rmSync(probeRoot, { recursive: true, force: true })
80+
}
81+
}
82+
const terminate = (signal) => {
83+
cleanup()
84+
process.kill(process.pid, signal)
85+
}
86+
const onSigint = () => terminate("SIGINT")
87+
const onSigterm = () => terminate("SIGTERM")
88+
process.once("SIGINT", onSigint)
89+
process.once("SIGTERM", onSigterm)
90+
91+
try {
92+
git(["worktree", "add", "--detach", probeRoot, worktreeCommit()])
93+
worktreeAdded = true
94+
95+
return {
96+
probeRoot,
97+
cleanup: () => {
98+
process.off("SIGINT", onSigint)
99+
process.off("SIGTERM", onSigterm)
100+
cleanup()
101+
},
102+
}
103+
} catch (error) {
104+
process.off("SIGINT", onSigint)
105+
process.off("SIGTERM", onSigterm)
106+
cleanup()
107+
throw error
108+
}
109+
}

0 commit comments

Comments
 (0)