Files
elizaos--eliza/packages/app-core/scripts/validate-regression-matrix.mjs
T
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

463 lines
13 KiB
JavaScript

/** Supports app-core build, packaging, or development orchestration for validate regression matrix mjs. */
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const APP_CORE_ROOT = path.resolve(SCRIPT_DIR, "..");
function explicitRepoRoot() {
const raw = process.env.ELIZA_REGRESSION_MATRIX_REPO_ROOT;
const explicitRoot = raw?.trim();
if (!explicitRoot) return null;
const repoRoot = path.resolve(explicitRoot);
if (
fs.existsSync(path.join(repoRoot, "package.json")) &&
fs.existsSync(path.join(repoRoot, ".github", "workflows"))
) {
return repoRoot;
}
throw new Error(
`Explicit regression matrix repository root is invalid: ${repoRoot}`,
);
}
function findRepoRoot(startDir) {
const configuredRoot = explicitRepoRoot();
if (configuredRoot) return configuredRoot;
try {
return execFileSync(
"git",
["-C", startDir, "rev-parse", "--show-toplevel"],
{
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
},
).trim();
} catch {
// Fall back to the legacy package/workflow sentinel walk for non-git exports.
}
let currentDir = startDir;
while (true) {
if (
fs.existsSync(path.join(currentDir, "package.json")) &&
fs.existsSync(path.join(currentDir, ".github", "workflows"))
) {
return currentDir;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
throw new Error(`Unable to resolve repository root from ${startDir}.`);
}
currentDir = parentDir;
}
}
const REPO_ROOT = findRepoRoot(APP_CORE_ROOT);
const MANIFEST_PATH = path.join(
APP_CORE_ROOT,
"test",
"regression-matrix.json",
);
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
function normalizeGuardMarker(marker) {
if (typeof marker === "string") return marker;
if (
Array.isArray(marker) &&
marker.every((part) => typeof part === "string")
) {
return marker.join("");
}
throw new Error(
`Regression matrix guard marker must be a string or string-part array: ${JSON.stringify(marker)}`,
);
}
function parsePathAliases(raw) {
if (!raw?.trim()) return [];
return raw
.split(",")
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const separatorIndex = entry.indexOf("=");
if (separatorIndex <= 0 || separatorIndex === entry.length - 1) {
throw new Error(
`Invalid regression matrix path alias "${entry}". Expected from=to.`,
);
}
return {
from: normalisePath(entry.slice(0, separatorIndex)),
to: normalisePath(entry.slice(separatorIndex + 1)),
};
});
}
const REPO_PATH_ALIASES = parsePathAliases(
process.env.ELIZA_REGRESSION_MATRIX_PATH_ALIASES ??
(process.env.ELIZA_REGRESSION_MATRIX_REPO_ROOT
? "packages/docs/=docs/"
: ""),
);
function parseArgs(argv) {
const args = new Map();
for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (!token.startsWith("--")) continue;
const next = argv[index + 1];
if (!next || next.startsWith("--")) {
args.set(token.slice(2), true);
continue;
}
args.set(token.slice(2), next);
index += 1;
}
return args;
}
function normalisePath(filePath) {
return filePath.split(path.sep).join("/");
}
function resolveRepoRelativePath(relativePath) {
const normalisedPath = normalisePath(relativePath);
const directPath = path.join(REPO_ROOT, normalisedPath);
if (fs.existsSync(directPath)) return normalisedPath;
for (const alias of REPO_PATH_ALIASES) {
if (!normalisedPath.startsWith(alias.from)) continue;
const aliasedPath = `${alias.to}${normalisedPath.slice(alias.from.length)}`;
if (fs.existsSync(path.join(REPO_ROOT, aliasedPath))) {
return aliasedPath;
}
}
const nestedElizaPath = path.join("eliza", normalisedPath);
if (fs.existsSync(path.join(REPO_ROOT, nestedElizaPath))) {
return nestedElizaPath;
}
return normalisedPath;
}
function globToRegExp(glob) {
const escaped = glob
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*\*/g, "\u0000")
.replace(/\*/g, "[^/]*")
.replace(/\?/g, "[^/]")
// biome-ignore lint/suspicious/noControlCharactersInRegex: NUL placeholder for glob ** conversion
.replace(/\u0000/g, ".*");
return new RegExp(`^${escaped}$`);
}
function matchesAnyGlob(filePath, globs) {
const normalisedPath = normalisePath(filePath);
return globs.some((glob) => globToRegExp(glob).test(normalisedPath));
}
function readText(relativePath) {
return fs.readFileSync(
path.join(REPO_ROOT, resolveRepoRelativePath(relativePath)),
"utf8",
);
}
function expandScheduledSuites(suiteIds) {
const expanded = new Set(suiteIds);
for (const suiteId of suiteIds) {
const providedSuites = manifest.suites[suiteId]?.provides ?? [];
for (const provided of providedSuites) {
expanded.add(provided);
}
}
return expanded;
}
function collectChangedFiles(args) {
const base =
args.get("base") ??
process.env.GITHUB_BASE_SHA ??
(process.env.GITHUB_BASE_REF
? `origin/${process.env.GITHUB_BASE_REF}`
: null);
const head = args.get("head") ?? process.env.GITHUB_SHA ?? "HEAD";
if (!base) {
return [];
}
const commands = [
["diff", "--name-only", `${base}...${head}`],
["diff", "--name-only", base, head],
];
for (const command of commands) {
try {
const raw = execFileSync("git", command, {
cwd: REPO_ROOT,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
return raw
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map(normalisePath);
} catch {
// Try the next diff strategy.
}
}
throw new Error(
`Unable to resolve changed files from base "${base}" to head "${head}".`,
);
}
function ensureWorkflowContracts(workflowName, failures) {
const workflowContract = manifest.workflowContracts[workflowName];
const workflowTexts = workflowContract.files.map((relativePath) => ({
relativePath,
text: readText(relativePath),
}));
for (const suiteId of workflowContract.scheduledSuites) {
const suite = manifest.suites[suiteId];
if (!suite) {
failures.push(
`Workflow "${workflowName}" references unknown suite "${suiteId}".`,
);
continue;
}
if (suite.command) {
const present = workflowTexts.some(({ text }) =>
text.includes(suite.command),
);
if (!present) {
failures.push(
`Workflow "${workflowName}" does not schedule "${suiteId}" via "${suite.command}".`,
);
}
}
if (suite.workflowCall) {
const present = workflowTexts.some(({ text }) =>
text.includes(suite.workflowCall),
);
if (!present) {
failures.push(
`Workflow "${workflowName}" does not reference "${suite.workflowCall}" for suite "${suiteId}".`,
);
}
}
for (const snippet of suite.requiredSnippets ?? []) {
const present = workflowTexts.some(({ text }) => text.includes(snippet));
if (!present) {
failures.push(
`Workflow "${workflowName}" is missing required snippet for "${suiteId}": ${snippet}`,
);
}
}
}
for (const snippet of workflowContract.bannedSnippets ?? []) {
const present = workflowTexts.some(({ text }) => text.includes(snippet));
if (present) {
failures.push(
`Workflow "${workflowName}" still contains banned inline snippet: ${snippet}`,
);
}
}
return expandScheduledSuites(workflowContract.scheduledSuites);
}
function ensurePackageScripts(failures) {
const packageJson = JSON.parse(readText("package.json"));
const scripts = packageJson.scripts ?? {};
for (const [scriptName, disallowedSnippets] of Object.entries(
manifest.guards.packageScriptDisallowlist ?? {},
)) {
const scriptBody = scripts[scriptName] ?? "";
for (const snippet of disallowedSnippets) {
if (scriptBody.includes(snippet)) {
failures.push(
`package.json script "${scriptName}" still contains stale snippet: ${snippet}`,
);
}
}
}
const deterministicE2E = scripts["test:e2e"] ?? "";
const heavyE2E = scripts["test:e2e:heavy"] ?? "";
for (const exception of manifest.exceptions.heavyOnlyE2E ?? []) {
if (!deterministicE2E.includes(`--exclude ${exception.path}`)) {
failures.push(
`test:e2e must explicitly exclude heavy-only path ${exception.path}.`,
);
}
if (!heavyE2E.includes(exception.path)) {
failures.push(
`test:e2e:heavy must explicitly include heavy-only path ${exception.path}.`,
);
}
}
}
function ensureDesktopInventory(failures) {
const checklistPath = path.join(
REPO_ROOT,
resolveRepoRelativePath(manifest.manualChecklistDoc),
);
if (!fs.existsSync(checklistPath)) {
failures.push(
`Manual desktop checklist is missing: ${manifest.manualChecklistDoc}`,
);
return;
}
const checklistText = fs.readFileSync(checklistPath, "utf8");
const inventoryTexts = (manifest.guards.desktopInventorySources ?? []).map(
(relativePath) => ({
relativePath,
text: readText(relativePath),
}),
);
const items = [
...(manifest.exceptions.desktopHeavyInventory ?? []),
...(manifest.exceptions.desktopManualChecklist ?? []),
];
const seenIds = new Set();
for (const item of items) {
if (seenIds.has(item.id)) {
failures.push(
`Desktop regression inventory item id is duplicated: ${item.id}`,
);
continue;
}
seenIds.add(item.id);
const presentInInventory = inventoryTexts.some(({ text }) =>
text.includes(item.description),
);
if (!presentInInventory) {
failures.push(
`Desktop regression inventory source does not reference "${item.description}".`,
);
}
}
for (const item of manifest.exceptions.desktopManualChecklist ?? []) {
if (!checklistText.includes(item.description)) {
failures.push(
`Manual desktop checklist is missing "${item.description}".`,
);
}
}
for (const { relativePath, text } of inventoryTexts) {
for (const rawMarker of manifest.guards.forbiddenDesktopInventoryMarkers ??
[]) {
const marker = normalizeGuardMarker(rawMarker);
if (text.includes(marker)) {
failures.push(
`${relativePath} still contains forbidden desktop inventory marker "${marker}".`,
);
}
}
}
}
function ensureChangedFileCoverage(
workflowName,
scheduledSuites,
failures,
args,
) {
const changedFiles = collectChangedFiles(args);
if (changedFiles.length === 0) {
console.log(
`No changed-file diff available for workflow "${workflowName}". Static contract checks only.`,
);
return;
}
const requiredSuites = new Set();
const matchedSurfaces = [];
for (const filePath of changedFiles) {
for (const surface of manifest.surfaces) {
if (!matchesAnyGlob(filePath, surface.globs)) continue;
matchedSurfaces.push(`${filePath} -> ${surface.name}`);
for (const suiteId of surface.workflowSuites?.[workflowName] ?? []) {
requiredSuites.add(suiteId);
}
}
}
if (matchedSurfaces.length > 0) {
console.log(`Matched regression surfaces for "${workflowName}":`);
for (const entry of matchedSurfaces) {
console.log(`- ${entry}`);
}
}
for (const suiteId of requiredSuites) {
if (!scheduledSuites.has(suiteId)) {
failures.push(
`Changed files require suite "${suiteId}" for workflow "${workflowName}", but that suite is not scheduled.`,
);
}
}
}
const args = parseArgs(process.argv.slice(2));
const workflowName = args.get("workflow");
if (
typeof workflowName !== "string" ||
!manifest.workflowContracts[workflowName]
) {
console.error(
`Usage: node scripts/validate-regression-matrix.mjs --workflow <${Object.keys(
manifest.workflowContracts,
).join("|")}> [--base <git-ref>] [--head <git-ref>]`,
);
process.exit(1);
}
const failures = [];
const scheduledSuites = ensureWorkflowContracts(workflowName, failures);
ensurePackageScripts(failures);
ensureDesktopInventory(failures);
ensureChangedFileCoverage(workflowName, scheduledSuites, failures, args);
if (failures.length > 0) {
console.error("\nRegression matrix validation failed:");
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log(
`Regression matrix validation passed for workflow "${workflowName}".`,
);