From 75992b77fb8e1253eb8e5e027dfb46e9126f60e7 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 2 Jun 2026 08:34:05 -0700 Subject: [PATCH] chore(ci): re-admit policy docs into ci-cache-hash before doc relocation docs/**/*.md is deny-listed from the CI cache hash (test-irrelevant). The CLAUDE.md restructure moves test/release POLICY into docs/TESTING.md + docs/RELEASING.md, which DO carry contracts the test suite reads. Without re-admitting them, a policy-only edit would produce the same cache hash and skip the test shard that runs the build-llms + doc-history guards (false-pass). Adds an ALLOW_PATTERNS re-admit step after the deny, scoped to the named policy docs (not a blanket docs un-deny). Lands FIRST, before any doc moves. Pinned by 3 new cases in test/scripts/ci-cache-hash.test.ts: TESTING.md + RELEASING.md edits MUST change the hash; docs/guide.md still must not. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/ci-cache-hash.sh | 29 ++++++++++++++++++++ test/scripts/ci-cache-hash.test.ts | 44 ++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/scripts/ci-cache-hash.sh b/scripts/ci-cache-hash.sh index ce05465ac..0e6f4af15 100755 --- a/scripts/ci-cache-hash.sh +++ b/scripts/ci-cache-hash.sh @@ -30,6 +30,14 @@ # - everything else under src/, test/, scripts/, .github/, package.json, # bun.lock, tsconfig*.json, the schema files — obviously test-affecting # +# POLICY-DOC RE-ADMIT (the docs/ exception): some docs/*.md files carry +# CI / release / test CONTRACTS that the test suite reads (e.g. the +# build-llms content-contract test, the doc-history guard). The broad +# `^docs/.*\.md$` deny above would let a policy edit to those skip CI — a +# false-pass. The ALLOW_PATTERNS list below re-admits them into the hash +# AFTER the deny. ADD a path there whenever you move a policy/contract doc +# under docs/ (current entries: docs/TESTING.md, docs/RELEASING.md). +# # Locale-stable: LC_ALL=C on the sort step so byte-order is identical # across runners (different default locales would re-order the line list # and change the final hash). @@ -113,6 +121,27 @@ DENY_RE=$(printf '\t(%s)' "$DENY_ALT") # TODOS\.md$|docs/.*\.md$|...)`. Each alternative anchors its own end. INCLUDED=$(printf '%s\n' "$LS_FILES" | grep -vE "$DENY_RE" || true) +# Re-admit test-affecting policy docs that live under docs/ but carry CI / +# release / test contracts. The broad `^docs/.*\.md$` deny above removed +# them; without this re-admit a policy edit to docs/TESTING.md or +# docs/RELEASING.md would produce the SAME hash and skip the test shard +# that runs the build-llms + doc-history guards — a false-pass. Patterns +# anchor on the `\t` boundary in `git ls-files -s` output, matching +# the deny-list convention above. Re-admitted lines that don't exist yet +# (pre-relocation) simply match nothing. +ALLOW_PATTERNS=( + '\tdocs/TESTING\.md$' + '\tdocs/RELEASING\.md$' +) +ALLOW_ALT="" +for p in "${ALLOW_PATTERNS[@]}"; do + if [ -z "$ALLOW_ALT" ]; then ALLOW_ALT="$p"; else ALLOW_ALT="$ALLOW_ALT|$p"; fi +done +READMIT=$(printf '%s\n' "$LS_FILES" | grep -E "($ALLOW_ALT)" || true) +if [ -n "$READMIT" ]; then + INCLUDED=$(printf '%s\n%s\n' "$INCLUDED" "$READMIT" | grep -v '^$' | LC_ALL=C sort -u) +fi + if [ -z "$INCLUDED" ]; then echo "error: every tracked file is deny-listed — refusing to hash empty set" >&2 exit 1 diff --git a/test/scripts/ci-cache-hash.test.ts b/test/scripts/ci-cache-hash.test.ts index fb55f1fa5..961b27b66 100644 --- a/test/scripts/ci-cache-hash.test.ts +++ b/test/scripts/ci-cache-hash.test.ts @@ -400,6 +400,50 @@ describe("ci-cache-hash.sh — edge cases", () => { }); }); +describe("ci-cache-hash.sh — policy-doc re-admit (test-affecting docs under docs/)", () => { + // These docs live under docs/ (normally deny-listed) but carry CI/release/ + // test contracts the suite reads. The re-admit must make edits to them + // invalidate the hash, WITHOUT un-denying ordinary docs. + const POLICY_FILES: Record = { + ...BASELINE_FILES, + "docs/TESTING.md": "# Testing\n\ntest tiers + isolation lint\n", + "docs/RELEASING.md": "# Releasing\n\nversion locations + ship process\n", + }; + + function withSandbox(test: (sb: Sandbox) => void) { + const sb = makeSandbox(POLICY_FILES); + try { + test(sb); + } finally { + rmSync(sb.dir, { recursive: true, force: true }); + } + } + + it("docs/TESTING.md edit MUST change hash (re-admitted policy doc)", () => { + withSandbox((sb) => { + const before = hash(sb); + modify(sb, "docs/TESTING.md", "# Testing\n\nNEW POLICY\n"); + expect(hash(sb)).not.toBe(before); + }); + }); + + it("docs/RELEASING.md edit MUST change hash (re-admitted policy doc)", () => { + withSandbox((sb) => { + const before = hash(sb); + modify(sb, "docs/RELEASING.md", "# Releasing\n\nNEW SHIP RULE\n"); + expect(hash(sb)).not.toBe(before); + }); + }); + + it("docs/guide.md edit STILL produces same hash (re-admit is scoped, not a blanket docs un-deny)", () => { + withSandbox((sb) => { + const before = hash(sb); + modify(sb, "docs/guide.md", "# Guide v3\n"); + expect(hash(sb)).toBe(before); + }); + }); +}); + describe("ci-cache-hash.sh — usage errors", () => { it("--bogus arg exits 2", () => { const r = spawnSync("bash", [SCRIPT_SRC, "--bogus"], {