diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index 8281d9d470..0bfdac0b17 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -30,6 +30,10 @@ "files": [ "bin", "lib", + "docs", + "schema", + "DEPLOYMENT.md", + "Dockerfile", "expected-engine.version" ], "scripts": { diff --git a/scripts/check-miner-package.mjs b/scripts/check-miner-package.mjs index 3c3dfabb5f..3ab81c3313 100644 --- a/scripts/check-miner-package.mjs +++ b/scripts/check-miner-package.mjs @@ -11,8 +11,20 @@ const ALLOWED = [ /^package\.json$/, /^README\.md$/, /^expected-engine\.version$/, + // Operational material shipped for `npm install -g` users so the quickstart doesn't require a repo visit (#4874): + /^DEPLOYMENT\.md$/, + /^Dockerfile$/, + /^docs\/[a-z0-9-]+\.md$/, + /^schema\/[a-z0-9.-]+\.json$/, +]; +const REQUIRED = [ + "bin/gittensory-miner.js", + "package.json", + // The operational files #4874 shipped — asserted present so they can never silently drop out of the package again. + "DEPLOYMENT.md", + "Dockerfile", + "schema/miner-goal-spec.schema.json", ]; -const REQUIRED = ["bin/gittensory-miner.js", "package.json"]; const FORBIDDEN_PATH = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i; const FORBIDDEN_CONTENT = /(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+|gts_[0-9a-f]{64}|[A-Z0-9_]*(TOKEN|SECRET|PRIVATE_KEY)=)/; @@ -31,6 +43,9 @@ export function validateMinerPackFileList(files, readContent) { if (!paths.some((file) => /^lib\/([a-z0-9-]+\/)?[a-z0-9-]+\.js$/.test(file))) { throw new Error("Miner package is missing lib/*.js artifacts"); } + if (!paths.some((file) => /^docs\/[a-z0-9-]+\.md$/.test(file))) { + throw new Error("Miner package is missing docs/*.md operational documentation"); + } return paths; } diff --git a/test/unit/check-miner-package.test.ts b/test/unit/check-miner-package.test.ts index 62cf075027..eb228ab1cb 100644 --- a/test/unit/check-miner-package.test.ts +++ b/test/unit/check-miner-package.test.ts @@ -85,7 +85,14 @@ describe("check-miner-package script", () => { it("rejects a package missing lib artifacts", () => { const result = runChecker({ - CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["package.json", "bin/gittensory-miner.js"]), + // Every REQUIRED file present so the check reaches (and fails on) the lib-artifacts guard specifically. + CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ + "package.json", + "bin/gittensory-miner.js", + "DEPLOYMENT.md", + "Dockerfile", + "schema/miner-goal-spec.schema.json", + ]), CHECK_MINER_PACK_TEST_CONTENT: "{}", }); expect(result.status).toBe(1); @@ -101,4 +108,64 @@ describe("check-miner-package script", () => { expect(result.status).toBe(1); expect(result.out).toContain("Secret-like content found in miner package file:"); }); + + describe("operational files (#4874)", () => { + // A complete, valid package including the operational material — DEPLOYMENT.md, the Dockerfile, a docs/*.md, + // and the schema — is accepted. + const FULL_PACKAGE = [ + "package.json", + "bin/gittensory-miner.js", + "lib/cli.js", + "DEPLOYMENT.md", + "Dockerfile", + "docs/coding-agent-driver.md", + "schema/miner-goal-spec.schema.json", + ]; + + it("accepts DEPLOYMENT.md, the Dockerfile, docs/*.md, and schema/*.json", () => { + const result = runChecker({ + CHECK_MINER_PACK_TEST_FILES: JSON.stringify(FULL_PACKAGE), + CHECK_MINER_PACK_TEST_CONTENT: "operational docs, nothing secret", + }); + expect(result.status).toBe(0); + expect(result.out).toMatch(/^Miner package dry-run ok:/); + expect(result.out).toContain("DEPLOYMENT.md"); + expect(result.out).toContain("docs/coding-agent-driver.md"); + expect(result.out).toContain("schema/miner-goal-spec.schema.json"); + }); + + it("requires DEPLOYMENT.md to be published (regression guard for #4874)", () => { + const result = runChecker({ + CHECK_MINER_PACK_TEST_FILES: JSON.stringify(FULL_PACKAGE.filter((f) => f !== "DEPLOYMENT.md")), + CHECK_MINER_PACK_TEST_CONTENT: "ok", + }); + expect(result.status).toBe(1); + expect(result.out).toContain("Miner package is missing required file: DEPLOYMENT.md"); + }); + + it("requires at least one docs/*.md file to be published", () => { + const result = runChecker({ + CHECK_MINER_PACK_TEST_FILES: JSON.stringify(FULL_PACKAGE.filter((f) => !f.startsWith("docs/"))), + CHECK_MINER_PACK_TEST_CONTENT: "ok", + }); + expect(result.status).toBe(1); + expect(result.out).toContain("Miner package is missing docs/*.md operational documentation"); + }); + + it("keeps the docs allowlist tight — a non-.md or nested docs file is still rejected", () => { + const nonMarkdown = runChecker({ + CHECK_MINER_PACK_TEST_FILES: JSON.stringify([...FULL_PACKAGE, "docs/notes.txt"]), + CHECK_MINER_PACK_TEST_CONTENT: "ok", + }); + expect(nonMarkdown.status).toBe(1); + expect(nonMarkdown.out).toContain("Unexpected file in miner package: docs/notes.txt"); + + const nested = runChecker({ + CHECK_MINER_PACK_TEST_FILES: JSON.stringify([...FULL_PACKAGE, "docs/nested/guide.md"]), + CHECK_MINER_PACK_TEST_CONTENT: "ok", + }); + expect(nested.status).toBe(1); + expect(nested.out).toContain("Unexpected file in miner package: docs/nested/guide.md"); + }); + }); });