diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 91d776d6..e3925e1a 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -33,7 +33,10 @@ on: - 'MARKETPLACE-SUBMISSION.md' - 'CURSOR-MARKETPLACE-SUBMISSION.md' - 'SUBMISSION-STATUS.md' - - '.github/workflows/test-suite.yml' + - '.github/**' + - '.gitattributes' + - '.gitignore' + - '.mise.toml' push: branches: [main] paths: @@ -56,7 +59,10 @@ on: - 'MARKETPLACE-SUBMISSION.md' - 'CURSOR-MARKETPLACE-SUBMISSION.md' - 'SUBMISSION-STATUS.md' - - '.github/workflows/test-suite.yml' + - '.github/**' + - '.gitattributes' + - '.gitignore' + - '.mise.toml' permissions: contents: read diff --git a/scripts/leak-scan.test.ts b/scripts/leak-scan.test.ts index 7b329c52..795b7493 100644 --- a/scripts/leak-scan.test.ts +++ b/scripts/leak-scan.test.ts @@ -3,7 +3,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { type LeakRule, ROOT_FILES, RULES, SURFACES, isObviousPlaceholderUuid, scanRepo, shippedFiles } from "./leak-scan.ts"; +import { execFileSync } from "node:child_process"; +import { type LeakRule, ROOT_FILES, RULES, SELF_EXEMPT, SURFACES, isObviousPlaceholderUuid, scanRepo, shippedFiles } from "./leak-scan.ts"; /** * The engine is tested with its own rules, not the shipped list. Two reasons: a @@ -322,3 +323,28 @@ test("the CI paths filter covers every surface the scan reads", () => { assert.ok(entries.includes(file), `root file "${file}" is not in the CI paths filter`); } }); + +test("every tracked file is scanned, or exempt with a reason", () => { + // The surface model enumerates what to read, so anything outside the enumeration is + // invisible — and an unread file reads as a clean one. That has shipped twice: 38 + // axiom-mcp files (Axiom-77q9) and later .gitignore, .gitattributes, .mise.toml and + // .github/**. Both were found by hand. This asserts the property instead, so the + // third omission fails here rather than waiting for someone to notice a gap. + const root = path.resolve(import.meta.dirname, ".."); + const tracked = execFileSync("git", ["-c", "core.quotepath=false", "ls-files"], { + cwd: root, + encoding: "utf8", + }) + .split("\n") + .filter(Boolean); + const scanned = new Set(shippedFiles(root)); + + const unread = tracked.filter((rel) => !scanned.has(rel) && !SELF_EXEMPT.has(rel)); + assert.deepEqual( + unread, + [], + `tracked file(s) in no scanned surface: ${unread.join(", ")}. Add the directory to ` + + `SURFACES or the file to ROOT_FILES in scripts/leak-scan.ts, and add it to the ` + + `workflow's paths filter — an unscanned file reads as a clean one.`, + ); +}); diff --git a/scripts/leak-scan.ts b/scripts/leak-scan.ts index b97e94bc..8c3326b1 100644 --- a/scripts/leak-scan.ts +++ b/scripts/leak-scan.ts @@ -83,12 +83,22 @@ export const SURFACES: Surface[] = [ { path: "docs", trackedOnly: true }, { path: "tools", trackedOnly: true }, { path: "scripts", trackedOnly: true }, + // Repo configuration ships publicly and can carry a local path (a tool config, a + // runner path in a workflow) as easily as source can. It was in no surface until a + // tracked-but-unscanned check found it; scripts/leak-scan.test.ts now fails when any + // tracked file is in neither a surface nor ROOT_FILES nor SELF_EXEMPT, so the next + // omission is caught rather than discovered. + { path: ".github", trackedOnly: true }, ]; /** * Root files are read from disk, not from the index: CHANGELOG.md is gitignored * here yet is rendered into the published site, so "tracked" is the wrong test * for them. + * + * The three dotfiles were added by the tracked-but-unscanned check. `.mise.toml` + * and `.gitignore` both name local paths in normal use, which is exactly what the + * absolute-home-path rule exists to catch. */ export const ROOT_FILES = [ "README.md", @@ -100,6 +110,9 @@ export const ROOT_FILES = [ "MARKETPLACE-SUBMISSION.md", "CURSOR-MARKETPLACE-SUBMISSION.md", "SUBMISSION-STATUS.md", + ".gitattributes", + ".gitignore", + ".mise.toml", ]; const SKIP_DIRS = new Set([ @@ -120,7 +133,7 @@ const SKIP_DIRS = new Set([ * legitimate place for the strings to appear. Exempting them by name is narrower * than weakening a pattern to avoid matching its own source. */ -const SELF_EXEMPT = new Set(["scripts/leak-scan.ts", "scripts/leak-scan.test.ts"]); +export const SELF_EXEMPT = new Set(["scripts/leak-scan.ts", "scripts/leak-scan.test.ts"]); export const RULES: LeakRule[] = [ {