e7738de6d2
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
184 lines
7.1 KiB
TypeScript
184 lines
7.1 KiB
TypeScript
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
|
|
// Every example package with a committed zero.graph must pass `zero check`,
|
|
// build an executable for the canonical linux-musl-x64 target, and `zero run`
|
|
// cleanly. This closes the gap where a stale example keeps passing check while
|
|
// run-compilation fails. Host-backend gaps (for example fs programs on the
|
|
// Mach-O direct backend) are tolerated only after the linux build proves the
|
|
// package is not stale; hosts whose backend supports the package still run it.
|
|
|
|
const zeroBin = resolve("bin/zero");
|
|
const examplesDir = resolve("examples");
|
|
const buildTarget = "linux-musl-x64";
|
|
const runTimeoutMs = 120_000;
|
|
const buildTimeoutMs = 300_000;
|
|
|
|
type Expectation = {
|
|
exitCode?: number;
|
|
output?: string;
|
|
server?: { readyText: string };
|
|
};
|
|
|
|
const expectations: Record<string, Expectation> = {
|
|
"batch3-cli": { output: "batch3 cli ok" },
|
|
"direct-package-arrays": { exitCode: 13 },
|
|
"direct-package-call-order": { exitCode: 27 },
|
|
"memory-package": { output: "memory package ok" },
|
|
"ping-pong-api": { server: { readyText: "listening on " } },
|
|
"readall-cli": { output: "readall cli ok" },
|
|
"resource-cli": { output: "resource cli ok" },
|
|
"systems-package": { output: "systems package" },
|
|
"zero-hash": { output: "zero-hash ok" },
|
|
};
|
|
|
|
const packages = readdirSync(examplesDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory() && existsSync(join(examplesDir, entry.name, "zero.graph")))
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
|
|
if (packages.length === 0) {
|
|
console.error("examples gate found no example packages with committed zero.graph stores");
|
|
process.exit(1);
|
|
}
|
|
|
|
const staleExpectations = Object.keys(expectations).filter((name) => !packages.includes(name));
|
|
if (staleExpectations.length > 0) {
|
|
console.error(`examples gate expectations name missing packages: ${staleExpectations.join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function runZero(args: string[], cwd: string, timeoutMs = runTimeoutMs) {
|
|
const result = spawnSync(zeroBin, args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
timeout: timeoutMs,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
return {
|
|
code: result.status,
|
|
signal: result.signal,
|
|
stdout: result.stdout ?? "",
|
|
stderr: result.stderr ?? "",
|
|
error: result.error?.message ?? "",
|
|
timedOut: result.error !== undefined && "code" in result.error && result.error.code === "ETIMEDOUT",
|
|
timeoutMs,
|
|
};
|
|
}
|
|
|
|
function zeroFailure(action: string, result: ReturnType<typeof runZero>) {
|
|
const detail = result.stderr || result.stdout || result.error;
|
|
if (result.timedOut) {
|
|
return `${action} timed out after ${result.timeoutMs}ms${detail ? `\n${detail}` : ""}`;
|
|
}
|
|
return `${action} failed (exit ${result.code}${result.signal ? ` signal ${result.signal}` : ""})${detail ? `\n${detail}` : ""}`;
|
|
}
|
|
|
|
function isHostBackendGap(text: string) {
|
|
return text.includes("BLD004") && text.includes("executable buildability subset");
|
|
}
|
|
|
|
async function probeServer(packagePath: string, readyText: string, workDir: string) {
|
|
return await new Promise<{ ok: boolean; detail: string }>((settle) => {
|
|
const child = spawn(zeroBin, ["run", packagePath], { cwd: workDir, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
let output = "";
|
|
let done = false;
|
|
const finish = (ok: boolean, detail: string) => {
|
|
if (done) return;
|
|
done = true;
|
|
clearTimeout(timer);
|
|
if (child.pid !== undefined && child.exitCode === null) {
|
|
try {
|
|
process.kill(-child.pid, "SIGKILL");
|
|
} catch {
|
|
child.kill("SIGKILL");
|
|
}
|
|
}
|
|
settle({ ok, detail });
|
|
};
|
|
const timer = setTimeout(() => finish(false, `server never printed '${readyText}'\n${output}`), runTimeoutMs);
|
|
const onChunk = (chunk: Buffer) => {
|
|
output += chunk.toString();
|
|
if (output.includes(readyText)) finish(true, "server ready");
|
|
};
|
|
child.stdout.on("data", onChunk);
|
|
child.stderr.on("data", onChunk);
|
|
child.on("error", (error) => finish(false, error.message));
|
|
child.on("exit", (code, signal) => finish(false, `server exited before readiness: code ${code} signal ${signal}\n${output}`));
|
|
});
|
|
}
|
|
|
|
const failures: string[] = [];
|
|
const startedAt = Date.now();
|
|
|
|
for (const name of packages) {
|
|
const packagePath = join(examplesDir, name);
|
|
const expectation = expectations[name] ?? {};
|
|
const workDir = mkdtempSync(join(tmpdir(), `zero-examples-gate-${name}-`));
|
|
const steps: string[] = [];
|
|
let failure = "";
|
|
|
|
const check = runZero(["check", packagePath], workDir);
|
|
if (check.code === 0) {
|
|
steps.push("check");
|
|
} else {
|
|
failure = zeroFailure("zero check", check);
|
|
}
|
|
|
|
let linuxBuildOk = false;
|
|
if (!failure && !expectation.server) {
|
|
const build = runZero(["build", "--emit", "exe", "--target", buildTarget, packagePath, "--out", join(workDir, `${name}-${buildTarget}`)], workDir, buildTimeoutMs);
|
|
if (build.code === 0) {
|
|
linuxBuildOk = true;
|
|
steps.push(`build ${buildTarget}`);
|
|
} else {
|
|
failure = zeroFailure(`zero build --target ${buildTarget}`, build);
|
|
}
|
|
}
|
|
|
|
if (!failure && expectation.server) {
|
|
const probe = await probeServer(packagePath, expectation.server.readyText, workDir);
|
|
if (probe.ok) {
|
|
steps.push("run server-ready");
|
|
} else {
|
|
failure = `zero run server probe failed: ${probe.detail}`;
|
|
}
|
|
} else if (!failure) {
|
|
const expectedExit = expectation.exitCode ?? 0;
|
|
// Arguments after the input belong to the program (zero run [input] -- <args>),
|
|
// so --out must precede the package path or it lands in the program argv.
|
|
const run = runZero(["run", "--out", join(workDir, `${name}-exe`), packagePath], workDir);
|
|
const runText = `${run.stdout}${run.stderr}`;
|
|
if (run.code !== expectedExit && linuxBuildOk && isHostBackendGap(runText)) {
|
|
steps.push("run host-backend-gap");
|
|
} else if (run.code !== expectedExit) {
|
|
failure = `zero run exited ${run.code}${run.signal ? ` signal ${run.signal}` : ""}, expected ${expectedExit}\n${runText}`;
|
|
} else if (expectation.output && !run.stdout.includes(expectation.output)) {
|
|
failure = `zero run output missing '${expectation.output}'\n${runText}`;
|
|
} else {
|
|
steps.push("run");
|
|
}
|
|
}
|
|
|
|
// Retry the teardown: macOS indexing can briefly repopulate the temp tree
|
|
// while rm walks it, surfacing as ENOTEMPTY on an otherwise clean removal.
|
|
rmSync(workDir, { force: true, recursive: true, maxRetries: 5, retryDelay: 100 });
|
|
if (failure) {
|
|
failures.push(`${name}: ${failure.trimEnd()}`);
|
|
console.error(`examples gate FAIL: ${name}`);
|
|
} else {
|
|
console.log(`examples gate ok: ${name} (${steps.join(", ")})`);
|
|
}
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`\nexamples gate failed for ${failures.length} package(s):\n`);
|
|
for (const failure of failures) console.error(`${failure}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`examples gate ok: ${packages.length} packages in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`);
|