diff --git a/package.json b/package.json index 97f990e22e..8f01efb5c9 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "build:mcp": "npm --workspace @jsonbored/gittensory-mcp run build", "build:miner": "npm --workspace @jsonbored/gittensory-engine run build && npm --workspace @jsonbored/gittensory-miner run build", "test:mcp-pack": "node scripts/check-mcp-package.mjs", + "test:miner-pack": "node scripts/check-miner-package.mjs", "rees:install": "npm ci --prefix review-enrichment --prefer-offline --no-audit --no-fund", "rees:test": "npm run rees:install && npm --prefix review-enrichment test", "rees:metadata": "npm --prefix review-enrichment run metadata", @@ -72,7 +73,7 @@ "test:smoke:observability": "node scripts/smoke-observability-traces.mjs", "test:smoke:browser:install": "playwright install chromium", "test:smoke:browser": "node scripts/smoke-ui-browser.mjs", - "test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", + "test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", "test:release": "npm run test:ci && npm run changelog:check", "test:release:mcp": "npm run test:ci && npm run changelog:check:mcp", "test:watch": "vitest", diff --git a/scripts/check-miner-package.mjs b/scripts/check-miner-package.mjs new file mode 100644 index 0000000000..aa48ddb948 --- /dev/null +++ b/scripts/check-miner-package.mjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const ALLOWED = [ + /^bin\/gittensory-miner\.js$/, + /^lib\/[a-z0-9-]+\.(js|d\.ts)$/, + /^package\.json$/, + /^README\.md$/, +]; +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)=)/; + +export function validateMinerPackFileList(files, readContent) { + const paths = files.map((file) => (typeof file === "string" ? file : file.path)).sort(); + for (const file of paths) { + if (FORBIDDEN_PATH.test(file)) throw new Error(`Forbidden file in miner package: ${file}`); + if (!ALLOWED.some((pattern) => pattern.test(file))) throw new Error(`Unexpected file in miner package: ${file}`); + const content = readContent(file); + if (FORBIDDEN_CONTENT.test(content)) throw new Error(`Secret-like content found in miner package file: ${file}`); + } + return paths; +} + +export function runMinerPackCheck(options = {}) { + const pack = options.pack ?? loadMinerPackFromNpm(); + const packageRoot = options.packageRoot ?? join(process.cwd(), "packages/gittensory-miner"); + const readContent = + options.readContent ?? + ((file) => { + if (process.env.CHECK_MINER_PACK_TEST_CONTENT !== undefined) return process.env.CHECK_MINER_PACK_TEST_CONTENT; + return readFileSync(join(packageRoot, file), "utf8"); + }); + const paths = validateMinerPackFileList(pack.files, readContent); + return `Miner package dry-run ok: ${paths.join(", ")}\n`; +} + +function loadMinerPackFromNpm() { + if (process.env.CHECK_MINER_PACK_TEST_FILES) { + const paths = JSON.parse(process.env.CHECK_MINER_PACK_TEST_FILES); + return { files: paths.map((path) => ({ path })) }; + } + const result = spawnSync("npm", ["pack", "--workspace", "@jsonbored/gittensory-miner", "--dry-run", "--json"], { + encoding: "utf8", + }); + if (result.status !== 0) { + const message = result.stderr || result.stdout || "npm pack failed"; + throw new Error(message.trim()); + } + return JSON.parse(result.stdout)[0]; +} + +function main() { + try { + process.stdout.write(runMinerPackCheck()); + } catch (err) { + process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) main(); diff --git a/test/unit/check-miner-package.test.ts b/test/unit/check-miner-package.test.ts new file mode 100644 index 0000000000..2725caba0b --- /dev/null +++ b/test/unit/check-miner-package.test.ts @@ -0,0 +1,46 @@ +import { execFileSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +function runChecker(env: Record = {}): { status: number; out: string } { + try { + const stdout = execFileSync(process.execPath, ["scripts/check-miner-package.mjs"], { + encoding: "utf8", + env: { ...process.env, ...env }, + }); + return { status: 0, out: stdout }; + } catch (err) { + const e = err as { status?: number; stdout?: string; stderr?: string }; + return { status: e.status ?? 1, out: `${e.stdout ?? ""}${e.stderr ?? ""}` }; + } +} + +describe("check-miner-package script", () => { + it("passes on the real miner workspace package", () => { + const result = runChecker(); + expect(result.status).toBe(0); + expect(result.out).toMatch(/^Miner package dry-run ok:/); + expect(result.out).toContain("bin/gittensory-miner.js"); + expect(result.out).toContain("package.json"); + }); + + it("rejects a forbidden path", () => { + const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([".env"]) }); + expect(result.status).toBe(1); + expect(result.out).toContain("Forbidden file in miner package: .env"); + }); + + it("rejects an unexpected file", () => { + const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["scripts/extra.mjs"]) }); + expect(result.status).toBe(1); + expect(result.out).toContain("Unexpected file in miner package: scripts/extra.mjs"); + }); + + it("rejects secret-like content", () => { + const result = runChecker({ + CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["package.json"]), + CHECK_MINER_PACK_TEST_CONTENT: "github_pat_abcdefghijklmnopqrstuvwxyz0123456789", + }); + expect(result.status).toBe(1); + expect(result.out).toContain("Secret-like content found in miner package file: package.json"); + }); +});