guanghu-ice-heart/server-tools/lake-lamp-continuity-guard/finalize-development.mjs

644 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const DEFAULT_GUARD =
"/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/continuity-lane.mjs";
const DEFAULT_STORE =
"/Volumes/JZAO/HoloLake/persona-runtime/continuity-memory/collaboration";
const CODE_CHANNEL_HOST = "guanghulab.com";
const CODE_CHANNEL_PREFIX = "/code/bingshuo/";
const CODE_CHANNEL_REPOSITORIES = new Set([
"guanghu-ice-heart",
"hololake-system-architecture",
"grok-build-upstream-mirror",
]);
const SAFE_CACHE_RELATIVE = [
/^target$/u,
/^coverage$/u,
/^\.nyc_output$/u,
/^\.pytest_cache$/u,
/^\.mypy_cache$/u,
/^\.ruff_cache$/u,
/^\.cache$/u,
/^\.next\/cache$/u,
/^node_modules\/\.cache$/u,
/(?:^|\/)__pycache__$/u,
];
const DEFAULT_CACHE_CANDIDATES = [
"target",
"coverage",
".nyc_output",
".pytest_cache",
".mypy_cache",
".ruff_cache",
".cache",
".next/cache",
"node_modules/.cache",
];
export function parseArgs(argv) {
const args = { receipts: [], cleanupPaths: [] };
for (let index = 0; index < argv.length; index += 1) {
const key = argv[index];
if (key === "--self-test") args.selfTest = true;
else if (key === "--receipt") args.receipts.push(argv[++index]);
else if (key === "--cleanup-path") args.cleanupPaths.push(argv[++index]);
else if (key.startsWith("--")) {
const name = key.slice(2).replaceAll(/-([a-z])/gu, (_, letter) =>
letter.toUpperCase(),
);
args[name] = argv[++index];
} else {
throw new Error(`UNKNOWN_ARGUMENT:${key}`);
}
}
return args;
}
export function normalizeCodeChannelRepository(value) {
let url;
try {
url = new URL(String(value || "").trim().replace(/^repo:\/\//u, "https://"));
} catch {
throw new Error("INVALID_CODE_CHANNEL_REPOSITORY");
}
if (url.username || url.password) {
throw new Error("CODE_CHANNEL_CREDENTIALS_FORBIDDEN");
}
const pathname = url.pathname.replace(/\.git$/u, "").replace(/\/+$/u, "");
const slug = pathname.startsWith(CODE_CHANNEL_PREFIX)
? pathname.slice(CODE_CHANNEL_PREFIX.length)
: "";
if (
url.protocol !== "https:" ||
url.hostname !== CODE_CHANNEL_HOST ||
!CODE_CHANNEL_REPOSITORIES.has(slug)
) {
throw new Error("NON_CANONICAL_CODE_CHANNEL_REPOSITORY");
}
return {
identity: `repo://${CODE_CHANNEL_HOST}${CODE_CHANNEL_PREFIX}${slug}`,
cloneUrl: `https://${CODE_CHANNEL_HOST}${CODE_CHANNEL_PREFIX}${slug}.git`,
slug,
};
}
export function normalizeBranch(value) {
const branch = String(value || "").trim();
if (
!branch ||
!/^(?!\/)(?!.*(?:\.\.|\/\/|@\{|[~^:?*\[\\]))[A-Za-z0-9._/-]+$/u.test(
branch,
) ||
branch.endsWith("/") ||
branch.endsWith(".")
) {
throw new Error("INVALID_TARGET_BRANCH");
}
return branch;
}
export function normalizeReceipt(value) {
const relative = String(value || "").replaceAll("\\", "/").replace(/^\.\/+/u, "");
if (
!relative.startsWith("deployment/receipts/") ||
relative.includes("/../") ||
relative.endsWith("/") ||
!/^[A-Za-z0-9._/-]+$/u.test(relative)
) {
throw new Error("INVALID_DEPLOYMENT_RECEIPT_PATH");
}
return relative;
}
export function safeCacheRelative(worktree, candidate) {
const root = fs.realpathSync(worktree);
const absolute = path.resolve(root, candidate);
const relative = path.relative(root, absolute).replaceAll(path.sep, "/");
if (
!relative ||
relative.startsWith("../") ||
path.isAbsolute(relative) ||
!SAFE_CACHE_RELATIVE.some((pattern) => pattern.test(relative))
) {
throw new Error(`CACHE_PATH_NOT_ALLOWLISTED:${candidate}`);
}
return { absolute, relative };
}
export function isJsonRecordName(name) {
return String(name).endsWith(".json") && !String(name).startsWith("._");
}
function run(file, args, options = {}) {
const output = execFileSync(file, args, {
cwd: options.cwd,
encoding: "utf8",
stdio: options.stdio || ["ignore", "pipe", "pipe"],
maxBuffer: 16 * 1024 * 1024,
});
return options.trim === false ? output : output.trim();
}
function git(worktree, args) {
return run("/usr/bin/git", ["-C", worktree, ...args]);
}
function gitRaw(worktree, args) {
return run("/usr/bin/git", ["-C", worktree, ...args], { trim: false });
}
function guard(guardScript, args) {
const output = run(process.execPath, [guardScript, ...args]);
return output ? JSON.parse(output) : null;
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
export function sha256(value) {
return crypto.createHash("sha256").update(value).digest("hex");
}
function listOwnedLeases(storeRoot, developmentId) {
const directory = path.join(storeRoot, "leases");
if (!fs.existsSync(directory)) return [];
return fs
.readdirSync(directory)
.filter(isJsonRecordName)
.map((name) => readJson(path.join(directory, name)))
.filter((lease) => lease.development_id === developmentId);
}
function ensureLane(storeRoot, developmentId, worktree) {
const lanePath = path.join(storeRoot, "lanes", `${developmentId}.json`);
const lane = readJson(lanePath);
if (lane.status !== "active" && lane.status !== "waiting") {
throw new Error(`LANE_NOT_RUNNABLE:${lane.status}`);
}
const owned = listOwnedLeases(storeRoot, developmentId);
const resolvedWorktree = fs.realpathSync(worktree);
if (
!owned.some((lease) => {
if (!path.isAbsolute(lease.resource)) return false;
try {
const resource = fs.realpathSync(lease.resource);
return (
resolvedWorktree === resource ||
resolvedWorktree.startsWith(`${resource}${path.sep}`) ||
resource.startsWith(`${resolvedWorktree}${path.sep}`)
);
} catch {
return false;
}
})
) {
throw new Error("WORKTREE_WRITE_LEASE_REQUIRED");
}
return lane;
}
function verifyWorktree(worktree, receipts) {
const root = fs.realpathSync(git(worktree, ["rev-parse", "--show-toplevel"]));
if (root !== fs.realpathSync(worktree)) {
throw new Error("WORKTREE_MUST_BE_GIT_ROOT");
}
if (git(root, ["status", "--porcelain=v1"])) {
throw new Error("WORKTREE_NOT_CLEAN");
}
const head = git(root, ["rev-parse", "HEAD"]);
if (!/^[0-9a-f]{40}$/u.test(head)) throw new Error("INVALID_LOCAL_HEAD");
const branch = normalizeBranch(git(root, ["branch", "--show-current"]));
const receiptEvidence = receipts.map((receipt) => {
const relative = normalizeReceipt(receipt);
const content = gitRaw(root, ["show", `HEAD:${relative}`]);
if (!content) throw new Error(`RECEIPT_NOT_COMMITTED:${relative}`);
return {
path: relative,
sha256: sha256(Buffer.from(content)),
bytes: Buffer.byteLength(content),
};
});
return { root, head, branch, receiptEvidence };
}
function startPublish({
guardScript,
developmentId,
repository,
branch,
}) {
const requested = guard(guardScript, [
"publish-request",
"--development-id",
developmentId,
"--repository",
repository.identity,
"--target",
branch,
"--priority",
"normal",
]);
guard(guardScript, ["publish-dispatch"]);
guard(guardScript, [
"publish-start",
"--development-id",
developmentId,
"--request-id",
requested.request_id,
]);
return requested.request_id;
}
function finishPublish(guardScript, developmentId, requestId, outcome, receipt) {
return guard(guardScript, [
"publish-finish",
"--development-id",
developmentId,
"--request-id",
requestId,
"--outcome",
outcome,
"--receipt",
receipt,
]);
}
function pushAndReadBack(worktree, repository, branch, head, receipts) {
run("/usr/bin/git", [
"-C",
worktree,
"fetch",
"--quiet",
"origin",
`refs/heads/${branch}`,
]);
const remoteBefore = git(worktree, [
"rev-parse",
`refs/remotes/origin/${branch}`,
]);
try {
git(worktree, ["merge-base", "--is-ancestor", remoteBefore, head]);
} catch {
throw new Error(`NON_FAST_FORWARD_REFUSED:${remoteBefore}`);
}
run("/usr/bin/git", [
"-C",
worktree,
"push",
"origin",
`HEAD:refs/heads/${branch}`,
]);
const remoteLine = run("/usr/bin/git", [
"ls-remote",
repository.cloneUrl,
`refs/heads/${branch}`,
]);
const remoteHead = remoteLine.split(/\s+/u)[0] || "";
if (remoteHead !== head) {
throw new Error(`REMOTE_HEAD_MISMATCH:${remoteHead || "missing"}`);
}
const readbackRoot = fs.mkdtempSync(
path.join(os.tmpdir(), "hololake-finalize-readback-"),
);
try {
const clone = path.join(readbackRoot, "repo");
run("/usr/bin/git", [
"clone",
"--quiet",
"--depth",
"1",
"--branch",
branch,
repository.cloneUrl,
clone,
]);
const cloneHead = git(clone, ["rev-parse", "HEAD"]);
if (cloneHead !== head) throw new Error(`FRESH_CLONE_HEAD_MISMATCH:${cloneHead}`);
git(clone, ["fsck", "--full"]);
for (const receipt of receipts) {
const remoteContent = gitRaw(clone, ["show", `HEAD:${receipt.path}`]);
if (sha256(Buffer.from(remoteContent)) !== receipt.sha256) {
throw new Error(`REMOTE_RECEIPT_MISMATCH:${receipt.path}`);
}
}
} finally {
fs.rmSync(readbackRoot, { recursive: true, force: true });
}
return { remoteBefore, remoteHead };
}
function activeProcessFor(absolute) {
const output = run("/bin/ps", ["-axo", "pid=,command="]);
return output
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.some((line) => {
const match = line.match(/^(\d+)\s+(.*)$/u);
if (!match) return false;
const pid = Number(match[1]);
return pid !== process.pid && pid !== process.ppid && match[2].includes(absolute);
});
}
function directoryBytes(root) {
let total = 0;
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const candidate = path.join(root, entry.name);
const stat = fs.lstatSync(candidate);
if (stat.isSymbolicLink()) continue;
if (stat.isDirectory()) total += directoryBytes(candidate);
else if (stat.isFile()) total += stat.size;
}
return total;
}
function collectPythonCaches(root, limit = 256) {
const found = [];
const walk = (directory, depth) => {
if (depth > 6 || found.length >= limit) return;
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
if (found.length >= limit) return;
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
const candidate = path.join(directory, entry.name);
if (entry.name === "__pycache__") {
found.push(path.relative(root, candidate));
continue;
}
if ([".git", "node_modules", "target", "build", "dist"].includes(entry.name)) {
continue;
}
walk(candidate, depth + 1);
}
};
walk(root, 0);
return found;
}
function cleanupCaches(worktree, explicitPaths) {
const candidates = [
...DEFAULT_CACHE_CANDIDATES,
...collectPythonCaches(worktree),
...explicitPaths,
];
const unique = new Map();
for (const candidate of candidates) {
const safe = safeCacheRelative(worktree, candidate);
unique.set(safe.absolute, safe);
}
const results = [];
for (const item of unique.values()) {
if (!fs.existsSync(item.absolute)) {
results.push({ path: item.relative, result: "SKIPPED_NOT_PRESENT", bytes: 0 });
continue;
}
const stat = fs.lstatSync(item.absolute);
if (stat.isSymbolicLink() || !stat.isDirectory()) {
throw new Error(`CACHE_TARGET_NOT_REAL_DIRECTORY:${item.relative}`);
}
if (git(worktree, ["ls-files", "--", item.relative])) {
throw new Error(`TRACKED_CACHE_REFUSED:${item.relative}`);
}
try {
git(worktree, ["check-ignore", "--no-index", "-q", "--", item.relative]);
} catch {
throw new Error(`UNIGNORED_CACHE_REFUSED:${item.relative}`);
}
if (activeProcessFor(item.absolute)) {
results.push({ path: item.relative, result: "SKIPPED_ACTIVE", bytes: 0 });
continue;
}
const bytes = directoryBytes(item.absolute);
fs.rmSync(item.absolute, { recursive: true, force: true });
results.push({ path: item.relative, result: "REMOVED", bytes });
}
return results;
}
function writeFinalizationReceipt(storeRoot, developmentId, payload) {
const directory = path.join(storeRoot, "finalization-receipts");
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
const filePath = path.join(
directory,
`${developmentId}-${new Date().toISOString().replaceAll(/[:.]/gu, "-")}.json`,
);
const temporary = `${filePath}.tmp-${process.pid}`;
fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, {
mode: 0o600,
});
fs.renameSync(temporary, filePath);
return filePath;
}
function releaseAndComplete({
guardScript,
storeRoot,
developmentId,
summary,
}) {
for (const lease of listOwnedLeases(storeRoot, developmentId)) {
guard(guardScript, [
"release",
"--development-id",
developmentId,
"--resource",
lease.resource,
]);
}
const lane = guard(guardScript, [
"complete",
"--development-id",
developmentId,
"--summary",
summary,
]);
const actions = guard(guardScript, ["completion-actions"]);
const current = actions.find(
(item) => item.development_id === developmentId,
);
if (!current) throw new Error("COMPLETION_ACTION_MISSING");
if ((current.delete_automation_ids || []).length === 0) {
guard(guardScript, [
"mark-completion-stopped",
"--development-id",
developmentId,
]);
return { lane_status: lane.status, notification_state: "stopped" };
}
return {
lane_status: lane.status,
notification_state: "stop_requested",
delete_automation_ids: current.delete_automation_ids,
};
}
export async function finalize(options) {
const developmentId = String(options.developmentId || "");
if (!/^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+$/u.test(developmentId)) {
throw new Error("INVALID_DEVELOPMENT_ID");
}
const guardScript = options.guardScript || DEFAULT_GUARD;
const storeRoot = options.storeRoot || DEFAULT_STORE;
const worktree = fs.realpathSync(options.worktree || "");
if (!options.receipts?.length) throw new Error("AT_LEAST_ONE_RECEIPT_REQUIRED");
ensureLane(storeRoot, developmentId, worktree);
const verified = verifyWorktree(worktree, options.receipts);
const repository = normalizeCodeChannelRepository(
options.repository || git(worktree, ["config", "--get", "remote.origin.url"]),
);
const branch = normalizeBranch(options.target || verified.branch);
if (branch !== verified.branch) throw new Error("CHECKED_OUT_BRANCH_MISMATCH");
let publishRequestId = null;
let publishStarted = false;
try {
publishRequestId = startPublish({
guardScript,
developmentId,
repository,
branch,
});
publishStarted = true;
const remote = pushAndReadBack(
worktree,
repository,
branch,
verified.head,
verified.receiptEvidence,
);
const publishReceipt =
`PASS_100 remote=${repository.identity}#${branch} ` +
`head=${remote.remoteHead} receipts=${verified.receiptEvidence
.map((item) => `${item.path}:${item.sha256}`)
.join(",")}`;
finishPublish(
guardScript,
developmentId,
publishRequestId,
"completed",
publishReceipt,
);
publishStarted = false;
const cleanup = cleanupCaches(worktree, options.cleanupPaths || []);
const removedBytes = cleanup
.filter((item) => item.result === "REMOVED")
.reduce((total, item) => total + item.bytes, 0);
const payload = {
schema: "hololake.development-finalization-receipt/v1",
development_id: developmentId,
result: "PASS_100",
code_channel: {
repository: repository.identity,
branch,
remote_head: remote.remoteHead,
fresh_clone_readback: 100,
git_fsck: 100,
receipts: verified.receiptEvidence,
},
cleanup: {
scope: "CURRENT_WORKTREE_IGNORED_ALLOWLISTED_CACHES_ONLY",
removed_bytes: removedBytes,
items: cleanup,
},
recorded_at: new Date().toISOString(),
};
const finalizationReceipt = writeFinalizationReceipt(
storeRoot,
developmentId,
payload,
);
const completion = releaseAndComplete({
guardScript,
storeRoot,
developmentId,
summary:
`自动收口PASS_100第五域代码频道${repository.identity}#${branch}` +
`远端回读${remote.remoteHead}${verified.receiptEvidence.length}份回执一致;` +
`本车道安全缓存清理${removedBytes}字节;回执${finalizationReceipt}`,
});
return { ...payload, finalization_receipt: finalizationReceipt, completion };
} catch (error) {
if (publishStarted && publishRequestId) {
try {
finishPublish(
guardScript,
developmentId,
publishRequestId,
"failed",
`FAIL_0 ${String(error.message || error).slice(0, 400)}`,
);
} catch {
// The original finalization failure remains authoritative.
}
}
throw error;
}
}
export function selfTest() {
const canonical = normalizeCodeChannelRepository(
"https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git",
);
if (
canonical.identity !==
"repo://guanghulab.com/code/bingshuo/guanghu-ice-heart"
) {
throw new Error("SELF_TEST_REPOSITORY_NORMALIZATION_FAILED");
}
for (const invalid of [
"https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git",
"https://user:secret@guanghulab.com/code/bingshuo/guanghu-ice-heart.git",
]) {
let rejected = false;
try {
normalizeCodeChannelRepository(invalid);
} catch {
rejected = true;
}
if (!rejected) throw new Error("SELF_TEST_INVALID_REPOSITORY_ACCEPTED");
}
if (
normalizeReceipt(
"deployment/receipts/GLW-CHJH-ROUTER-CONTROLLER-JD-20260805.json",
) !==
"deployment/receipts/GLW-CHJH-ROUTER-CONTROLLER-JD-20260805.json"
) {
throw new Error("SELF_TEST_RECEIPT_NORMALIZATION_FAILED");
}
return { result: "PASS_100" };
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const result = args.selfTest
? selfTest()
: await finalize({
developmentId: args.developmentId,
worktree: args.worktree,
repository: args.repository,
target: args.target,
receipts: args.receipts,
cleanupPaths: args.cleanupPaths,
guardScript: args.guardScript,
storeRoot: args.storeRoot,
});
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
process.stderr.write(`FINALIZATION_FAIL_0:${String(error.message || error)}\n`);
process.exitCode = 1;
});
}