Files
workinf_Blender_Wasm/tools/web/run-wbv2-continuous.mjs
mes123456 51d313357c
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
fix(web): make continuous handoff self-recovering
2026-08-30 14:05:10 +08:00

112 lines
6.4 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
const read = (file) => fs.readFileSync(abs(file), "utf8");
const maxArg = process.argv.indexOf("--max-iterations");
const maxIterations = maxArg >= 0 ? Number(process.argv[maxArg + 1]) : 0;
if (!Number.isSafeInteger(maxIterations) || maxIterations < 0) throw new Error("--max-iterations must be a non-negative integer; 0 means unlimited");
const commitEnabled = process.argv.includes("--commit");
const pushEnabled = process.argv.includes("--push");
if (pushEnabled && !commitEnabled) throw new Error("--push requires --commit");
const pushRetryArg = process.argv.indexOf("--push-retries");
const pushRetries = pushRetryArg >= 0 ? Number(process.argv[pushRetryArg + 1]) : 6;
if (!Number.isSafeInteger(pushRetries) || pushRetries < 0) throw new Error("--push-retries must be a non-negative integer");
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
function waitMs(milliseconds) {
if (milliseconds <= 0) return;
const wait = new Int32Array(new SharedArrayBuffer(4));
Atomics.wait(wait, 0, 0, milliseconds);
}
function command(command, logPath) {
const result = spawnSync("bash", ["-lc", command], { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
if (logPath) fs.writeFileSync(abs(logPath), output);
return { code: result.status ?? 1, output };
}
function currentTask() {
const queue = read("docs/EXECUTION_QUEUE.md");
const task = queue.match(/\| 当前任务 \| `(WBV2-P\d+-\d+)` \|/u)?.[1];
const parentManifest = queue.match(/\| parent manifest \| `([^`]+)` \|/u)?.[1];
if (!task || !parentManifest) throw new Error("queue pointer is incomplete");
return { task, parentManifest };
}
function taskCommand(task) {
const card = read(`docs/tasks/${task}.md`);
const match = card.match(/- Implementation entrypoint: `([^`]+)`/u);
if (!match) throw new Error(`task card has no implementation entrypoint: ${task}`);
return match[1];
}
function assertContext() {
const result = command("node tools/web/print-task-context.mjs && node tools/web/check-task-context.mjs", "tests/golden/WBV2/continuous-context.log");
if (result.code !== 0) throw new Error(`context gate failed (exit ${result.code})`);
}
let iterations = 0;
const summary = [];
while (maxIterations === 0 || iterations < maxIterations) {
const before = currentTask();
assertContext();
const focused = taskCommand(before.task);
const logPath = `tests/golden/${before.task}/continuous-runner.log`;
const focusedResult = command(focused, logPath);
if (focusedResult.code !== 0) throw new Error(`${before.task} focused command failed (exit ${focusedResult.code}); log=${logPath}`);
const after = currentTask();
if (after.task === before.task) throw new Error(`${before.task} focused command exited 0 without advancing queue; log=${logPath}`);
const gates = command("node tools/web/check-context-governance.mjs && node tools/web/check-execution-control-plane.mjs --allow-unrunnable && node tools/web/check-v2-task-index.mjs && node tools/web/check-v2-handoff.mjs && npm --prefix web run typecheck && npm --prefix web run lint && git diff --check", `tests/golden/${before.task}/continuous-gates.log`);
if (gates.code !== 0) throw new Error(`${before.task} post-handoff gate failed (exit ${gates.code}); log=tests/golden/${before.task}/continuous-gates.log`);
iterations += 1;
summary.push({ task: before.task, command: focused, nextTask: after.task, log: logPath });
process.stdout.write(`continuous-iteration=${iterations} task=${before.task} next=${after.task}\n`);
if (commitEnabled) {
const manifestPath = `tests/golden/${before.task}/manifest.json`;
const artifactPaths = fs.existsSync(abs(manifestPath))
? Object.values(JSON.parse(read(manifestPath).toString()).artifacts ?? {}).map((artifact) => artifact?.path).filter((file) => typeof file === "string")
: [];
const handoffPaths = [
"docs/EXECUTION_QUEUE.md",
"nextTask.md",
`docs/tasks/${before.task}.md`,
`docs/tasks/${after.task}.md`,
`docs/status/${before.task}.md`,
`docs/status/${after.task}.md`,
manifestPath,
...artifactPaths,
"tests/golden/WBV2/task-index.json",
"tests/golden/WBV2/task-catalog.jsonl",
].filter((file, index, all) => file && all.indexOf(file) === index && fs.existsSync(abs(file)));
const commit = command(`git add ${handoffPaths.map(shellQuote).join(" ")}`, `tests/golden/${before.task}/continuous-commit.log`);
if (commit.code !== 0) throw new Error(`${before.task} staging failed; log=tests/golden/${before.task}/continuous-commit.log`);
const commitResult = command(`git diff --cached --quiet || git commit -m "chore(web): continuous handoff ${before.task}"`, `tests/golden/${before.task}/continuous-commit-result.log`);
if (commitResult.code !== 0) throw new Error(`${before.task} commit failed; log=tests/golden/${before.task}/continuous-commit-result.log`);
if (pushEnabled) {
let pushed = false;
for (let attempt = 0; attempt <= pushRetries; attempt += 1) {
const pushLog = attempt === 0
? `tests/golden/${before.task}/continuous-push.log`
: `tests/golden/${before.task}/continuous-push-attempt-${attempt + 1}.log`;
const push = command("git push origin main", pushLog);
const pushVerification = command(
"local_head=$(git rev-parse --verify HEAD) && remote_head=$(git ls-remote --refs origin refs/heads/main | awk '$2 == \"refs/heads/main\" { print $1; exit }') && printf 'local_head=%s remote_head=%s\\n' \"$local_head\" \"$remote_head\" && test -n \"$remote_head\" && test \"$local_head\" = \"$remote_head\"",
`tests/golden/${before.task}/continuous-push-verify.log`,
);
if (pushVerification.code === 0) {
pushed = true;
break;
}
if (attempt < pushRetries) waitMs(Math.min(30000, 1000 * 2 ** attempt));
}
if (!pushed) throw new Error(`${before.task} remote push verification failed after ${pushRetries + 1} attempts; log=tests/golden/${before.task}/continuous-push-verify.log`);
}
}
}
process.stdout.write(`continuous-run-complete iterations=${iterations} current=${currentTask().task} committed=${commitEnabled} pushed=${pushEnabled}\n`);