Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

522 lines
18 KiB
JavaScript

#!/usr/bin/env node
/**
* Proof job for the exhaustive develop lane (#12342). Fails loudly when the
* committed lane manifest and the real workflow/test-plan drift apart, so the
* scheduled full-matrix run cannot silently drop coverage or report vacuous
* green.
*
* It cross-checks four independent sources of truth:
* 1. `packages/scripts/ci-lane-manifest.json` — the committed expectation.
* 2. `.github/workflows/test.yml` — every manifest lane must exist as a job
* and must not be gated so it can never run on the exhaustive (non-PR)
* event, which would turn a "required" lane into a permanent skip.
* 3. `.github/workflows/develop-exhaustive.yml` — the scheduled orchestrator
* must still invoke every manifest `reusableWorkflows` lane via
* `workflow_call`, and each of those workflows must still declare a
* `workflow_call` trigger without unconditional concurrency cancellation.
* A dropped `uses:`, removed trigger, or reusable lane that can cancel a
* prior scheduled run silently strips platform coverage (Windows/mobile/
* scenario/UI/desktop) from the exhaustive matrix and fails the job.
* 4. `run-all-tests.mjs --plan=json` — the discovered task plan must clear the
* manifest floors (total tasks/packages, per-script-lane presence, and the
* set of required core packages). A pointed-at-a-nonexistent-glob lane or a
* deleted core package collapses one of these and fails the job.
*
* Usage:
* node packages/scripts/ci-full-matrix-proof.mjs [--plan-file <path>]
* [--manifest <path>]
* [--summary <path>]
*
* `--plan-file` short-circuits the plan discovery (used by tests and by CI when
* the plan was captured in an earlier step). Without it the script spawns the
* runner in `--plan=json` mode itself. Exit code 0 = every lane accounted for;
* non-zero = at least one drift, with every violation printed (not just the
* first) and mirrored into the GitHub step summary when `--summary` is given.
*/
import { spawnSync } from "node:child_process";
import {
appendFileSync,
closeSync,
mkdtempSync,
openSync,
readFileSync,
rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "..", "..");
function parseArgs(argv) {
const options = {
planFile: null,
manifest: resolve(here, "ci-lane-manifest.json"),
summary: process.env.GITHUB_STEP_SUMMARY || null,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--plan-file") {
options.planFile = argv[(i += 1)];
} else if (arg === "--manifest") {
options.manifest = argv[(i += 1)];
} else if (arg === "--summary") {
options.summary = argv[(i += 1)];
} else {
throw new Error(`unknown argument: ${arg}`);
}
if (
(arg === "--plan-file" || arg === "--manifest" || arg === "--summary") &&
argv[i] === undefined
) {
throw new Error(`${arg} requires a value`);
}
}
return options;
}
function loadManifest(manifestPath) {
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
if (!Array.isArray(manifest.workflowLanes)) {
throw new Error(`${manifestPath}: workflowLanes must be an array`);
}
if (!manifest.planFloors || typeof manifest.planFloors !== "object") {
throw new Error(`${manifestPath}: planFloors must be an object`);
}
return manifest;
}
function loadPlan({ planFile }) {
if (planFile) {
return JSON.parse(readFileSync(planFile, "utf8"));
}
// Redirect the runner's stdout to a file rather than capturing it through a
// pipe. The plan JSON is >64KB and run-all-tests calls process.exit(0) right
// after writing it, which can truncate a piped stdout mid-flush; a file
// descriptor is flushed on close, so this is the only lossless capture. It
// also mirrors how the CI workflow invokes the runner (`> plan.json`).
const runner = resolve(here, "run-all-tests.mjs");
const dir = mkdtempSync(join(tmpdir(), "ci-full-matrix-proof-"));
const planPath = join(dir, "plan.json");
const fd = openSync(planPath, "w");
let result;
try {
result = spawnSync(process.execPath, [runner, "--plan=json"], {
cwd: repoRoot,
stdio: ["ignore", fd, "pipe"],
});
} finally {
closeSync(fd);
}
try {
if (result.status !== 0) {
throw new Error(
`run-all-tests.mjs --plan=json exited ${result.status}: ${
result.stderr ? result.stderr.toString() : ""
}`,
);
}
return JSON.parse(readFileSync(planPath, "utf8"));
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
// Locate a top-level job block in the workflow YAML by its key. Returns the raw
// text of that job (up to the next top-level 2-space-indented key) or null.
// A structural regex read keeps this dependency-free; test.yml is hand-authored
// with the conventional two-space job indentation this relies on.
function extractJobBlock(workflowText, jobKey) {
const lines = workflowText.split(/\r?\n/);
const header = ` ${jobKey}:`;
const start = lines.findIndex((line) => line === header);
if (start < 0) return null;
const body = [lines[start]];
for (let i = start + 1; i < lines.length; i += 1) {
const line = lines[i];
if (/^ {2}\S/.test(line) && !/^ {3}/.test(line)) break;
body.push(line);
}
return body.join("\n");
}
function extractWorkflowJobKeys(workflowText) {
const keys = [];
for (const line of workflowText.split(/\r?\n/)) {
const match = line.match(/^ {2}([A-Za-z0-9_-]+):\s*$/);
if (match) keys.push(match[1]);
}
return keys;
}
function parseNeeds(jobBlock) {
const lines = jobBlock.split(/\r?\n/);
const needs = new Set();
const needsLineIndex = lines.findIndex((line) => /^ {4}needs:\s*/.test(line));
if (needsLineIndex < 0) return needs;
const inline = lines[needsLineIndex].replace(/^ {4}needs:\s*/, "").trim();
if (inline.startsWith("[") && inline.endsWith("]")) {
for (const value of inline
.slice(1, -1)
.split(",")
.map((item) => item.trim().replace(/^['"]|['"]$/g, ""))
.filter(Boolean)) {
needs.add(value);
}
return needs;
}
if (inline) {
needs.add(inline.replace(/^['"]|['"]$/g, ""));
return needs;
}
for (let i = needsLineIndex + 1; i < lines.length; i += 1) {
const line = lines[i];
if (/^ {4}\S/.test(line)) break;
const match = line.match(/^ {6}-\s*([A-Za-z0-9_-]+)\s*$/);
if (match) needs.add(match[1]);
}
return needs;
}
function buildNeedsGraph(workflowText) {
const graph = new Map();
for (const jobKey of extractWorkflowJobKeys(workflowText)) {
const block = extractJobBlock(workflowText, jobKey);
if (block) graph.set(jobKey, parseNeeds(block));
}
return graph;
}
function collectTransitiveNeeds(graph, root) {
const visited = new Set();
const stack = [...(graph.get(root) ?? [])];
while (stack.length > 0) {
const job = stack.pop();
if (!job || visited.has(job)) continue;
visited.add(job);
for (const next of graph.get(job) ?? []) {
if (!visited.has(next)) stack.push(next);
}
}
return visited;
}
// A lane job is "exhaustively runnable" when its `if:` does not force a skip on
// non-PR events. The repo convention gates PR runs with
// `github.event_name != 'pull_request' || needs.changes.outputs.<x> == 'true'`,
// which is TRUE on schedule/push/merge_group. A job with no `if:` always runs.
// The only failure we can catch statically is an `if:` that hard-pins the job to
// pull_request only (so the exhaustive event would skip it).
function laneRunsOnExhaustiveEvent(jobBlock) {
const ifMatch = jobBlock.match(/^\s{4}if:\s*(.+)$/m);
if (!ifMatch) return true;
const condition = ifMatch[1].trim();
// Hard PR-only pin: e.g. `if: github.event_name == 'pull_request'` with no
// non-PR escape. If the condition mentions pull_request equality but never the
// inequality/other-event escape, treat it as PR-pinned.
const pinsToPullRequest =
/github\.event_name\s*==\s*'pull_request'/.test(condition) &&
!/github\.event_name\s*!=\s*'pull_request'/.test(condition) &&
!/'(push|schedule|merge_group|workflow_dispatch)'/.test(condition);
return !pinsToPullRequest;
}
function checkWorkflowLanes(manifest, violations, laneReport) {
const workflowPath = resolve(repoRoot, manifest.workflow);
const workflowText = readFileSync(workflowPath, "utf8");
for (const lane of manifest.workflowLanes) {
const jobBlock = extractJobBlock(workflowText, lane.job);
if (jobBlock === null) {
violations.push(
`missing lane: job "${lane.job}" (${lane.name}) not found in ${manifest.workflow}`,
);
laneReport.push({ lane: lane.job, name: lane.name, status: "MISSING" });
continue;
}
if (!laneRunsOnExhaustiveEvent(jobBlock)) {
violations.push(
`unexpectedly skipped lane: job "${lane.job}" (${lane.name}) is pinned to pull_request only and cannot run on the exhaustive scheduled event`,
);
laneReport.push({ lane: lane.job, name: lane.name, status: "PR-ONLY" });
continue;
}
laneReport.push({ lane: lane.job, name: lane.name, status: "OK" });
}
// The aggregate status job must exist and must `needs:` every workflow lane so
// a lane cannot silently drop out of the required check.
if (manifest.aggregateStatusJob) {
const aggregate = extractJobBlock(
workflowText,
manifest.aggregateStatusJob,
);
if (aggregate === null) {
violations.push(
`missing aggregate: job "${manifest.aggregateStatusJob}" not found in ${manifest.workflow}`,
);
} else {
const graph = buildNeedsGraph(workflowText);
const reachable = collectTransitiveNeeds(
graph,
manifest.aggregateStatusJob,
);
for (const lane of manifest.workflowLanes) {
if (!reachable.has(lane.job)) {
violations.push(
`aggregate drift: job "${manifest.aggregateStatusJob}" does not need lane "${lane.job}" (${lane.name}) directly or through an aggregate dependency`,
);
}
}
}
}
}
// The scheduled exhaustive orchestrator (develop-exhaustive.yml) must keep
// invoking every platform lane the manifest lists, and each of those workflows
// must still declare a `workflow_call` trigger — otherwise the orchestrator
// silently drops that lane's coverage. Both halves are checked statically so a
// dropped `uses:` or a removed trigger fails without waiting for the run.
function checkReusableWorkflows(manifest, violations, laneReport) {
if (
!manifest.exhaustiveOrchestrator ||
!Array.isArray(manifest.reusableWorkflows)
) {
return;
}
const orchestratorPath = resolve(repoRoot, manifest.exhaustiveOrchestrator);
let orchestratorText;
try {
orchestratorText = readFileSync(orchestratorPath, "utf8");
} catch {
violations.push(
`missing exhaustive orchestrator: ${manifest.exhaustiveOrchestrator} not found`,
);
return;
}
for (const reusable of manifest.reusableWorkflows) {
const basename = reusable.workflow.split("/").pop();
const usesRef = `./.github/workflows/${basename}`;
if (!orchestratorText.includes(`uses: ${usesRef}`)) {
violations.push(
`missing reusable lane: ${manifest.exhaustiveOrchestrator} does not invoke ${usesRef} (${reusable.name})`,
);
laneReport.push({
lane: basename,
name: reusable.name,
status: "NOT-WIRED",
});
continue;
}
let reusableText;
let declaresWorkflowCall = false;
try {
reusableText = readFileSync(resolve(repoRoot, reusable.workflow), "utf8");
declaresWorkflowCall = /^\s{2}workflow_call:/m.test(reusableText);
} catch {
violations.push(
`missing reusable workflow: ${reusable.workflow} (${reusable.name}) not found`,
);
laneReport.push({
lane: basename,
name: reusable.name,
status: "MISSING",
});
continue;
}
if (!declaresWorkflowCall) {
violations.push(
`reusable workflow not callable: ${reusable.workflow} does not declare a workflow_call trigger, so ${manifest.exhaustiveOrchestrator} cannot invoke it`,
);
laneReport.push({
lane: basename,
name: reusable.name,
status: "NO-CALL",
});
continue;
}
if (/^\s{2}cancel-in-progress:\s*true\s*$/m.test(reusableText)) {
violations.push(
`reusable workflow can cancel scheduled coverage: ${reusable.workflow} has unconditional cancel-in-progress: true; gate cancellation to pull_request so ${manifest.exhaustiveOrchestrator} remains uncancellable`,
);
laneReport.push({
lane: basename,
name: reusable.name,
status: "CANCELS",
});
continue;
}
laneReport.push({ lane: basename, name: reusable.name, status: "OK" });
}
}
function checkPlanFloors(manifest, plan, violations, floorReport) {
const floors = manifest.planFloors;
const summary = plan.summary || {};
const tasks = Array.isArray(plan.tasks) ? plan.tasks : [];
const taskCount = summary.taskCount ?? tasks.length;
const packageCount =
summary.packageCount ?? new Set(tasks.map((t) => t.packageName)).size;
floorReport.push({
metric: "taskCount",
value: taskCount,
floor: floors.minTaskCount,
});
if (
typeof floors.minTaskCount === "number" &&
taskCount < floors.minTaskCount
) {
violations.push(
`plan floor: taskCount ${taskCount} < minTaskCount ${floors.minTaskCount} (a lane matched no tests?)`,
);
}
floorReport.push({
metric: "packageCount",
value: packageCount,
floor: floors.minPackageCount,
});
if (
typeof floors.minPackageCount === "number" &&
packageCount < floors.minPackageCount
) {
violations.push(
`plan floor: packageCount ${packageCount} < minPackageCount ${floors.minPackageCount}`,
);
}
if (typeof floors.minPluginTaskCount === "number") {
const pluginTasks = tasks.filter((t) =>
String(t.relativeDir || "").startsWith("plugins/"),
).length;
floorReport.push({
metric: "pluginTaskCount",
value: pluginTasks,
floor: floors.minPluginTaskCount,
});
if (pluginTasks < floors.minPluginTaskCount) {
violations.push(
`plan floor: pluginTaskCount ${pluginTasks} < minPluginTaskCount ${floors.minPluginTaskCount}`,
);
}
}
const presentPackages = new Set(tasks.map((t) => t.packageName));
for (const required of floors.requiredPackages || []) {
if (!presentPackages.has(required)) {
violations.push(
`plan floor: required package "${required}" has no discovered test task (deleted, renamed, or its test script vanished)`,
);
}
}
const byScript = summary.byScript || {};
for (const laneScript of floors.nonEmptyScriptLanes || []) {
const count = byScript[laneScript] ?? 0;
floorReport.push({
metric: `script:${laneScript}`,
value: count,
floor: 1,
});
if (count < 1) {
violations.push(
`plan floor: script lane "${laneScript}" collected zero tasks (whole ${laneScript} lane vanished)`,
);
}
}
}
function writeSummary(summaryPath, laneReport, floorReport, violations) {
if (!summaryPath) return;
const lines = [];
lines.push("## Exhaustive lane matrix proof");
lines.push("");
lines.push("### Workflow lanes");
lines.push("");
lines.push("| Lane (job) | Name | Status |");
lines.push("| --- | --- | --- |");
for (const row of laneReport) {
lines.push(`| \`${row.lane}\` | ${row.name} | ${row.status} |`);
}
lines.push("");
lines.push("### Plan floors");
lines.push("");
lines.push("| Metric | Value | Floor |");
lines.push("| --- | --- | --- |");
for (const row of floorReport) {
lines.push(`| ${row.metric} | ${row.value} | ${row.floor} |`);
}
lines.push("");
if (violations.length === 0) {
lines.push(
"**Result: PASS** — every expected lane is present and non-empty.",
);
} else {
lines.push(`**Result: FAIL** — ${violations.length} violation(s):`);
lines.push("");
for (const violation of violations) {
lines.push(`- ${violation}`);
}
}
lines.push("");
appendFileSync(summaryPath, `${lines.join("\n")}\n`);
}
export function runProof(options) {
const manifest = loadManifest(options.manifest);
const plan = loadPlan(options);
const violations = [];
const laneReport = [];
const floorReport = [];
checkWorkflowLanes(manifest, violations, laneReport);
checkReusableWorkflows(manifest, violations, laneReport);
checkPlanFloors(manifest, plan, violations, floorReport);
return { manifest, plan, violations, laneReport, floorReport };
}
function main() {
let options;
try {
options = parseArgs(process.argv.slice(2));
} catch (error) {
console.error(`[ci-full-matrix-proof] ERROR ${error.message}`);
process.exit(2);
}
const { violations, laneReport, floorReport } = runProof(options);
for (const row of laneReport) {
console.log(`[ci-full-matrix-proof] lane ${row.lane}${row.status}`);
}
for (const row of floorReport) {
console.log(
`[ci-full-matrix-proof] floor ${row.metric}=${row.value} (min ${row.floor})`,
);
}
writeSummary(options.summary, laneReport, floorReport, violations);
if (violations.length > 0) {
console.error(
`[ci-full-matrix-proof] FAIL ${violations.length} violation(s):`,
);
for (const violation of violations) {
console.error(` - ${violation}`);
}
process.exit(1);
}
console.log("[ci-full-matrix-proof] PASS every expected lane accounted for");
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}