Public source validation / validate (push) Failing after 3m8s
128 lines
6.2 KiB
JavaScript
128 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
function fail(message) {
|
|
console.error(message);
|
|
process.exit(1);
|
|
}
|
|
|
|
function argument(name) {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
}
|
|
|
|
const root = fs.realpathSync(argument("--repository") ?? process.cwd());
|
|
const outputArgument = argument("--output");
|
|
const reportArgument = argument("--report");
|
|
const allowlistPath = path.resolve(root, argument("--allowlist") ?? "public-source.allowlist");
|
|
if (!outputArgument) fail("Usage: node scripts/export-public-source.mjs --output <new-directory> [--report <file>]");
|
|
|
|
const output = path.resolve(outputArgument);
|
|
const reportPath = reportArgument ? path.resolve(reportArgument) : undefined;
|
|
if (output === root || output.startsWith(`${root}${path.sep}`)) fail("The public export must be outside the canonical repository.");
|
|
if (fs.existsSync(output)) fail(`Output already exists: ${output}`);
|
|
if (!fs.existsSync(path.join(root, "LICENSE"))) fail("Public export blocked: confirm the license proposal and add LICENSE first.");
|
|
|
|
const status = execFileSync("git", ["-C", root, "status", "--porcelain=v1", "--untracked-files=all"], { encoding: "utf8" });
|
|
if (status.trim()) fail("Public export blocked: the canonical repository must be clean and committed.");
|
|
|
|
const allowlist = fs.readFileSync(allowlistPath, "utf8")
|
|
.split(/\r?\n/u)
|
|
.map((line) => line.trim())
|
|
.filter((line) => line && !line.startsWith("#"));
|
|
|
|
function selected(relativePath) {
|
|
return allowlist.some((rule) => rule.endsWith("/**") ? relativePath.startsWith(rule.slice(0, -2)) : relativePath === rule);
|
|
}
|
|
|
|
const deniedPrefixes = [".agents/", ".claude/", ".codex/", "artifacts/", "design/", "planning/", "prompts/"];
|
|
const deniedRootFiles = new Set(["AGENTS.md", "AUDIT.md", "CURRENT_STATE.md", "DECISIONS.md", "MASTER_PROMPT.txt", "PACKAGE_MANIFEST.md", "PACKAGE_REPORT.md", "PLANS.md", "ROADMAP.md", "START_HERE.md"]);
|
|
function denied(relativePath) {
|
|
return deniedRootFiles.has(relativePath)
|
|
|| path.posix.basename(relativePath) === "AGENTS.md"
|
|
|| deniedPrefixes.some((prefix) => relativePath.startsWith(prefix));
|
|
}
|
|
|
|
const replacements = [
|
|
[/https?:\/\/pulse\.itworx\.tech/giu, "https://pulse.example.com"],
|
|
[/\bgitea\.itworx\.tech\b/giu, "git.example.com"],
|
|
[/\b192\.168\.10\.150\b/gu, "192.0.2.10"],
|
|
[/\bTower\.local\b/gu, "unraid.example.test"],
|
|
[/\/mnt\/user\/appdata\/itworx-pulse/gu, "/srv/pulse"],
|
|
[/C:\\Users\\Jens\\/gu, "C:\\Users\\example\\"]
|
|
];
|
|
|
|
function renderedBytes(sourceBytes) {
|
|
if (sourceBytes.includes(0)) return sourceBytes;
|
|
let value = sourceBytes.toString("utf8");
|
|
for (const [expression, replacement] of replacements) value = value.replace(expression, replacement);
|
|
return Buffer.from(value, "utf8");
|
|
}
|
|
|
|
const privatePatterns = [
|
|
{ id: "private-domain", expression: /\b(?:pulse|gitea)\.itworx\.tech\b/iu },
|
|
{ id: "private-hostname", expression: /\bTower\.local\b/iu },
|
|
{ id: "private-lan-address", expression: /\b192\.168\.10\.150\b/u },
|
|
{ id: "operator-appdata-path", expression: /\/mnt\/user\/appdata\/itworx-pulse/iu },
|
|
{ id: "personal-windows-path", expression: /[A-Z]:\\Users\\Jens\\/iu },
|
|
{ id: "private-repository-owner", expression: /(?:git@[^\s:]+:|https?:\/\/[^\s/]+\/)Jens\/ITWorx-Pulse/iu }
|
|
];
|
|
|
|
const tracked = execFileSync("git", ["-C", root, "ls-files", "-z"], { encoding: "utf8" })
|
|
.split("\0").filter(Boolean).map((entry) => entry.replaceAll("\\", "/"));
|
|
const files = tracked.filter((relativePath) => selected(relativePath) && !denied(relativePath))
|
|
.sort((left, right) => left.localeCompare(right, "en"));
|
|
if (files.length === 0) fail("The allowlist selected no tracked files.");
|
|
|
|
const findings = [];
|
|
const rendered = new Map();
|
|
for (const relativePath of files) {
|
|
const source = path.join(root, ...relativePath.split("/"));
|
|
const stat = fs.lstatSync(source);
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
findings.push({ rule: "non-regular-file", path: relativePath });
|
|
continue;
|
|
}
|
|
if (stat.size > 5 * 1024 * 1024) {
|
|
findings.push({ rule: "oversized-file", path: relativePath, bytes: stat.size });
|
|
continue;
|
|
}
|
|
const bytes = renderedBytes(fs.readFileSync(source));
|
|
rendered.set(relativePath, bytes);
|
|
if (!bytes.includes(0)) {
|
|
const text = bytes.toString("utf8");
|
|
for (const rule of privatePatterns) if (rule.expression.test(text)) findings.push({ rule: rule.id, path: relativePath });
|
|
}
|
|
}
|
|
|
|
for (const required of ["README.md", "SECURITY.md", "CONTRIBUTING.md", "LICENSE", "go.mod", "package.json", "deploy/compose.yaml", ".gitea/workflows/public-validation.yml"]) {
|
|
if (!files.includes(required)) findings.push({ rule: "missing-required-file", path: required });
|
|
}
|
|
|
|
const sourceRevision = execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
const report = { schemaVersion: 1, sourceRevision, selectedFiles: files.length, findings };
|
|
if (reportPath) {
|
|
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
}
|
|
if (findings.length > 0) fail(`Public export blocked by ${findings.length} finding(s). See ${reportPath ?? "the scan output"}.`);
|
|
|
|
fs.mkdirSync(output, { recursive: false });
|
|
const manifest = [];
|
|
for (const relativePath of files) {
|
|
const bytes = rendered.get(relativePath);
|
|
const destination = path.join(output, ...relativePath.split("/"));
|
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
fs.writeFileSync(destination, bytes);
|
|
manifest.push({ path: relativePath, bytes: bytes.length, sha256: crypto.createHash("sha256").update(bytes).digest("hex") });
|
|
}
|
|
|
|
fs.writeFileSync(path.join(output, "PUBLIC_SOURCE_EXPORT.md"), `# Curated public source export\n\nGenerated from private canonical revision \`${sourceRevision}\`.\n\nThis parentless candidate excludes private operational history, evidence, planning, prompts, and machine-local agent configuration.\n`, "utf8");
|
|
fs.writeFileSync(path.join(output, "PUBLIC_SOURCE_MANIFEST.json"), `${JSON.stringify({ schemaVersion: 1, sourceRevision, files: manifest }, null, 2)}\n`, "utf8");
|
|
console.log(`Exported ${files.length} reviewed files to ${output}`);
|