ci: filter test matrices by per-test coverage globs (#730)
* ci: filter test matrices by per-test coverage globs to cut LLM spend every test in `crossagent/`, `agnostic/`, and every provider entry now declares a `coverage: string[]` of repo-relative globs. the new `changes` job runs `paths-filter` for a docs-only short-circuit, then pipes the changed-file list into `action/test/matrix.ts`, which intersects each entry's coverage against the diff and emits filtered `agents`, `agnostic`, `flagships`, and `aliases` matrices. main pushes and `workflow_dispatch` set `FULL=1` to run everything as a stale-glob safety net. retires `changed-agents.sh` and the `MODE=flagships` branch in `list-aliases.ts` in favor of one consistent model. * ci(matrix): switch test discovery to dep-free static parsing the GHA `changes` job has no `node_modules` installed. the previous dynamic-import path pulled the test files transitively through `utils.ts` -> `agents/index.ts` -> `@actions/core`, which exploded with ERR_MODULE_NOT_FOUND. parse the test files via regex instead so matrix.ts stays zero-dep — the chain (matrix -> coverage / providers / list-aliases / models) imports only node builtins and relative TS files. * ci(matrix): address PR #730 review feedback - drop dangling `action/mcp/toolFiltering.ts` glob from `nobash`, `restricted`, `tokenExfil` (file doesn't exist; `.test.ts` does, but the runtime tooling lives in `mcp/shell.ts` and `agents/{claude,opencode}.ts`, both already covered). - drop unused `coverageForProvider` export and its `byName` map from `providers.ts` (matrix.ts builds its own lookup inline). - derive the active agent list from `agents/index.ts` via the same dep-free regex tactic as `parseTestFile` instead of hardcoding `["claude", "opencode"]` — adding a new harness file now wires it into the dynamic matrix automatically. - treat `coverage: []` as `coverage: undefined` in `shouldRun` so an accidentally-empty array doesn't silently skip CI on every PR. - add `action/utils/activity.ts` and `action/mcp/selectMode.ts` to the `timeout` test's coverage — the activity-timeout enforcement path was the original reason the test exists. - ungate the `root` job (lint/format/typecheck/vitest). it's a required status check on `main`, so gating it on `code == 'true'` would make docs-only PRs unmergeable (skipped jobs don't satisfy required-check rules). the real LLM savings come from skipping the four matrices, not from skipping `root`. - harden the four matrix-job `if:` guards from `outputs.matrix && ...` to `outputs.matrix != '' && ...` — explicit > implicit short-circuit. - document `expandBraces`'s flat-only support so a future author isn't surprised by `{a,{b,c}}` not expanding. - fix awkward sentence in `wiki/action-tests.md` "CI Cost Filtering".
This commit is contained in:
committed by
pullfrog[bot]
parent
4ad649ebb9
commit
1f4c3031be
+20
-51
@@ -1,4 +1,3 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -16,7 +15,7 @@ type WorkflowJob = {
|
||||
"runs-on": string;
|
||||
"timeout-minutes"?: number;
|
||||
permissions?: WorkflowPermissions;
|
||||
strategy?: { "fail-fast": boolean; matrix: Record<string, string[]> };
|
||||
strategy?: { "fail-fast": boolean; matrix: Record<string, unknown> };
|
||||
env?: Record<string, string>;
|
||||
steps?: unknown[];
|
||||
};
|
||||
@@ -57,7 +56,6 @@ const expectedAgents = Object.keys(agents).sort();
|
||||
const crossagentTests = getTestNamesFromDir("crossagent");
|
||||
const agnosticTests = getTestNamesFromDir("agnostic");
|
||||
const adhocTests = getTestNamesFromDir("adhoc");
|
||||
const dynamicAgentsExpression = "$" + "{{ fromJSON(needs.changes.outputs.agents) }}";
|
||||
|
||||
// all provider API key names + GITHUB_TOKEN + model overrides
|
||||
const expectedAgentEnvVars = [
|
||||
@@ -83,53 +81,22 @@ describe("ci workflow consistency", () => {
|
||||
const rootJob = rootWorkflow.jobs["action-agents"];
|
||||
const actionJob = actionWorkflow.jobs.agents;
|
||||
|
||||
it("root agent matrix uses dynamic output from changes job", () => {
|
||||
expect(rootJob.strategy!.matrix.agent).toBe(dynamicAgentsExpression);
|
||||
});
|
||||
|
||||
it("changed-agents.sh falls back to opencode when shared agent code changed", () => {
|
||||
const input = JSON.stringify(["action/agents/shared.ts"]);
|
||||
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
|
||||
input,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
expect(JSON.parse(output)).toEqual(["opencode"]);
|
||||
});
|
||||
|
||||
it("changed-agents.sh falls back to opencode for non-agent action changes", () => {
|
||||
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
|
||||
input: JSON.stringify(["action/mcp/server.ts"]),
|
||||
encoding: "utf-8",
|
||||
});
|
||||
expect(JSON.parse(output)).toEqual(["opencode"]);
|
||||
});
|
||||
|
||||
it("changed-agents.sh includes opencode canary alongside changed agents", () => {
|
||||
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
|
||||
input: JSON.stringify(["action/agents/opencode.ts", "action/mcp/server.ts"]),
|
||||
encoding: "utf-8",
|
||||
});
|
||||
expect(JSON.parse(output)).toEqual(["opencode"]);
|
||||
});
|
||||
|
||||
it("changed-agents.sh treats legacy agent files as non-agent changes", () => {
|
||||
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
|
||||
input: JSON.stringify(["action/agents/codex.ts", "action/agents/gemini.ts"]),
|
||||
encoding: "utf-8",
|
||||
});
|
||||
expect(JSON.parse(output)).toEqual(["opencode"]);
|
||||
it("root agents matrix is wired to the dynamic matrix output", () => {
|
||||
const include = rootJob.strategy?.matrix.include;
|
||||
expect(typeof include).toBe("string");
|
||||
expect(include as string).toContain("fromJSON(needs.changes.outputs.matrix).agents");
|
||||
});
|
||||
|
||||
it("action agent matrix matches agents map", () => {
|
||||
expect([...actionJob.strategy!.matrix.agent].sort()).toEqual(expectedAgents);
|
||||
});
|
||||
|
||||
it("root test matrix matches crossagent/ directory", () => {
|
||||
expect([...rootJob.strategy!.matrix.test].sort()).toEqual(crossagentTests);
|
||||
expect((actionJob.strategy?.matrix.agent as string[])?.slice().sort()).toEqual(
|
||||
expectedAgents
|
||||
);
|
||||
});
|
||||
|
||||
it("action test matrix matches crossagent/ directory", () => {
|
||||
expect([...actionJob.strategy!.matrix.test].sort()).toEqual(crossagentTests);
|
||||
expect((actionJob.strategy?.matrix.test as string[])?.slice().sort()).toEqual(
|
||||
crossagentTests
|
||||
);
|
||||
});
|
||||
|
||||
it("permissions match between root and action", () => {
|
||||
@@ -149,8 +116,8 @@ describe("ci workflow consistency", () => {
|
||||
});
|
||||
|
||||
it("fail-fast is enabled in both", () => {
|
||||
expect(rootJob.strategy!["fail-fast"]).toBe(true);
|
||||
expect(actionJob.strategy!["fail-fast"]).toBe(true);
|
||||
expect(rootJob.strategy?.["fail-fast"]).toBe(true);
|
||||
expect(actionJob.strategy?.["fail-fast"]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,12 +125,14 @@ describe("ci workflow consistency", () => {
|
||||
const rootJob = rootWorkflow.jobs["action-agnostic"];
|
||||
const actionJob = actionWorkflow.jobs.agnostic;
|
||||
|
||||
it("root test matrix matches agnostic/ directory", () => {
|
||||
expect([...rootJob.strategy!.matrix.test].sort()).toEqual(agnosticTests);
|
||||
it("root agnostic matrix is wired to the dynamic matrix output", () => {
|
||||
const include = rootJob.strategy?.matrix.include;
|
||||
expect(typeof include).toBe("string");
|
||||
expect(include as string).toContain("fromJSON(needs.changes.outputs.matrix).agnostic");
|
||||
});
|
||||
|
||||
it("action test matrix matches agnostic/ directory", () => {
|
||||
expect([...actionJob.strategy!.matrix.test].sort()).toEqual(agnosticTests);
|
||||
expect((actionJob.strategy?.matrix.test as string[])?.slice().sort()).toEqual(agnosticTests);
|
||||
});
|
||||
|
||||
it("permissions match between root and action", () => {
|
||||
@@ -183,8 +152,8 @@ describe("ci workflow consistency", () => {
|
||||
});
|
||||
|
||||
it("fail-fast is enabled in both", () => {
|
||||
expect(rootJob.strategy!["fail-fast"]).toBe(true);
|
||||
expect(actionJob.strategy!["fail-fast"]).toBe(true);
|
||||
expect(rootJob.strategy?.["fail-fast"]).toBe(true);
|
||||
expect(actionJob.strategy?.["fail-fast"]).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user