Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

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}`);