76879b27ec
* docker testing rewrite: bake the image, drop the allowlist, kill the quoting - new `pnpm gha <script>` wrapper. one entry point for running any node script in the GHA-like container; replaces the runtime apt-get + useradd + chown ceremony in `action/utils/docker.ts`. - `action/Dockerfile` bakes ubuntu:24.04 + node 24 + gh + jq + sudo + testuser at uid 1000. `action/docker-entrypoint.sh` remaps to the host uid/gid and `exec`s the requested command — no `bash -c` nesting, no `escapeForDoubleQuotes`. - env passthrough: full `process.env` (+ `.env` via dotenv) flows through `--env-file`, multi-line values via `-e` fallback. drops `EnvFilterMode` / `testEnvAllowList`. - image rebuild is content-hash gated on Dockerfile + entrypoint; volume is versioned by hash so a stale `node_modules` cache from an old image can't poison a new one. - `action/play.ts` slimmed to a CLI; `run()` extracted to `action/utils/runFixture.ts`. drops the `--local` / `PLAY_LOCAL` dual mode in favor of explicit `play:local` / `runtest:local` scripts. - `action/test/run.ts` no longer self-relaunches into docker — that's `gha`'s job now. - `action/test/coverage.ts` `ALWAYS_RUN_ALL` updated to track the new files. - `wiki/docker.md` rewritten (243 → 105 lines). `wiki/action-tests.md`, `wiki/billing.md`, `wiki/adversarial.md`, `README.md`, `AGENTS.md` all updated to drop `--local` / `PLAY_LOCAL` references. verified end-to-end: `pnpm play` runs the default fixture against pullfrog/scratch, exit 0; `sudo unshare --pid` still works inside the container; `pnpm runtest` boots through the wrapper. * gha: address review feedback + 3 related issues found locally review-flagged: - bare `pnpm gha --build` now builds the image and exits 0 (was printing help and exiting 1 — docs claimed it was a valid standalone) - `initVolumeOwnership` skipped when the named volume already exists; saves the ~240ms `docker run … chown` on every warm invocation - `GIT_SSH_COMMAND` gate widened to any `id_*` private key (was hard- coded to `id_rsa`, leaving ed25519-only linux contributors with the default ssh config). dropped `-i` so ssh picks whichever key exists - new `action/.dockerignore` — partial mitigation noted: BuildKit (default since docker 23) only sends files referenced by the Dockerfile (~42B in practice), so the perf concern is mostly hypothetical. file is still worth keeping for `DOCKER_BUILDKIT=0` fallback and as documented intent for future `COPY . .` additions related issues found while validating locally: - `parseArgs` now stops flag-parsing at the first positional (or literal `--`); `pnpm gha test/run.ts --build` previously intercepted `--build` as a gha flag instead of forwarding to `test/run.ts` - new `pnpm gha --clean` command prunes orphan `pullfrog-gha:*` images and `pullfrog-gha-node-modules-*` volumes whose hash doesn't match the current Dockerfile (each Dockerfile/entrypoint edit creates a fresh hash and orphans the prior pair, ~600MB + ~200MB each — without a cleaner they accumulate silently) - `--shell` without a TTY now fails fast with an actionable message before docker is invoked, instead of producing the confusing `the input device is not a TTY` from docker run wiki updated: documents `--clean`, the parseArgs passthrough rule, and a new "Reclaiming disk" section. * gha: fidelity, flexibility, and signal-safety improvements investigated local fidelity vs the real GHA ubuntu-24.04 runner and addressed the gaps that have actually bitten contributors or could. fidelity (image now matches GHA closer): - bake build-essential, wget, xz-utils, file alongside the existing toolset. gh, jq, git, python3, sudo, ssh, build-essential, wget, xz, file, unzip, curl all present. native module builds (node-gyp, any package missing arm64 prebuilts) now work; common agent shell calls don't hit ENOENT - `host.docker.internal:host-gateway` flag wires the host into the container's DNS on linux (macOS Docker Desktop bakes it in). lets scripts that hit a local dev server use `API_URL=http://host.docker. internal:3100` and work identically on both platforms - `--init` makes tini PID 1, fixing signal forwarding during the pre-exec warmup window (Ctrl-C was previously taking up to 10s to tear down because bash-as-PID-1 swallowed the signal) - pnpm version is correctly pinned via the workspace's `packageManager` field — corepack resolves it at install time; verified via the new `--doctor` command flexibility (new affordances): - `pnpm gha --doctor` runs an inside-the-container fidelity audit: os + arch + node/pnpm/python versions, version snapshots of every baked tool, env vars (CI, HOME, TMPDIR), uid/gid, and the host.docker.internal resolution. useful for "works in CI fails locally" or vice versa - `pnpm gha --build --no-cache` busts the docker layer cache when an apt mirror, base image, or external download has changed upstream - entrypoint's `pnpm install` warmup is now wrapped in a `flock` on a file in the shared node_modules volume — concurrent `pnpm gha` invocations (e.g. play in one terminal, runtest in another) serialize their install instead of racing docs: - new "Gaps (known)" section in wiki/docker.md explicitly calling out the things this system can't do yet, including the missing `uses: ./action` semantics gap that `.github/workflows/action-gha-e2e-adhoc.yml` currently fills via GHA only (designing a local `pnpm gha-action <fixture>` is on the roadmap), service containers, parallel-run sharing, and arch differences (arm64 vs amd64) * docs: audit + corrections after testing fronts self-audit pass for stale references and incomplete pointers: - wiki/browser.md: `Docker (node:24)` → `pnpm gha container (ubuntu:24.04)`. the substance was right (chrome not preinstalled) but the base image reference was stale. - wiki/docker.md: the "Permission errors" troubleshooting line claimed the node_modules volume is chowned on every run; now correctly says "owned by the host uid on first creation; warm runs skip the chown" to match the actual behavior after the initVolumeOwnership fix. - wiki/action-tests.md: `API_URL` env-var doc now mentions BOTH paths (`localhost:` from play:local, `host.docker.internal:` from inside the container). Proxy/router recipe now shows both invocations side-by-side instead of saying "must use play:local". - wiki/billing.md: same dual-recipe update for the loop-including-the- action proxy walkthrough. - gha.ts header: expanded the usage block to include --clean / --doctor / --no-cache / --shell-TTY, added the host.docker.internal note, and pointed at wiki/docker.md for design rationale. self-document check: a future agent landing on this code can answer "how do I run a fixture / debug in shell / add a tool / diagnose fidelity / reach a local dev server" purely from gha.ts header + wiki/docker.md without spelunking through the entrypoint or git history.
117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
/**
|
|
* shared coverage / glob plumbing for the matrix builder.
|
|
*
|
|
* every test (`crossagent/`, `agnostic/`) and every provider entry
|
|
* (`providers.ts`) declares a `coverage` array of repo-relative globs. on a PR
|
|
* push, the `changes` job feeds the changed-file list into `matrix.ts`, which
|
|
* intersects each entry's globs against the diff and emits only the entries
|
|
* that need to run.
|
|
*
|
|
* `ALWAYS_RUN_ALL` is the escape hatch: any change to a file matched here
|
|
* forces the full matrix (every test, every flagship, every alias). it
|
|
* captures cross-cutting infrastructure where fan-out is unpredictable —
|
|
* agent loader, MCP server boot, test runner itself. if a per-test glob
|
|
* goes stale, this list and the on-`main`-full-matrix policy are the safety
|
|
* nets — there's no completeness lint.
|
|
*
|
|
* `coverage` is optional on tests/providers; missing = always run (treat as
|
|
* "any code change touches me"). default to defensive — opt into precision
|
|
* by adding globs.
|
|
*/
|
|
|
|
/** patterns that, when matched by any changed file, force the full matrix. */
|
|
export const ALWAYS_RUN_ALL: string[] = [
|
|
// agent loader + cross-agent shared code
|
|
"action/agents/shared.ts",
|
|
"action/agents/index.ts",
|
|
"action/agents/postRun.ts",
|
|
// test harness — changing these can affect every test
|
|
"action/test/run.ts",
|
|
"action/test/utils.ts",
|
|
"action/test/matrix.ts",
|
|
"action/test/coverage.ts",
|
|
"action/test/providers.ts",
|
|
// boot + lifecycle
|
|
"action/main.ts",
|
|
"action/index.ts",
|
|
"action/cli.ts",
|
|
"action/utils/setup.ts",
|
|
"action/utils/lifecycle.ts",
|
|
"action/utils/install.ts",
|
|
"action/utils/runFixture.ts",
|
|
"action/utils/globals.ts",
|
|
// GHA-like container plumbing (changes invalidate every test's environment)
|
|
"action/Dockerfile",
|
|
"action/docker-entrypoint.sh",
|
|
"action/gha.ts",
|
|
// MCP orchestrator (every test runs through it)
|
|
"action/mcp/server.ts",
|
|
"action/mcp/shared.ts",
|
|
// dependency graph
|
|
"action/package.json",
|
|
"action/pnpm-lock.yaml",
|
|
// workflow itself
|
|
".github/workflows/test.yml",
|
|
];
|
|
|
|
/**
|
|
* expand a single brace group like `{a,b,c}` into an array of patterns.
|
|
*
|
|
* intentionally minimal: nested braces (`{a,{b,c}}`) and escaped braces are
|
|
* NOT supported — coverage globs in this repo only need flat brace groups
|
|
* (`{claude,opencode}.ts`). add complexity if a real use case emerges.
|
|
*/
|
|
function expandBraces(pattern: string): string[] {
|
|
const m = pattern.match(/\{([^{}]+)\}/);
|
|
if (!m || m.index === undefined) return [pattern];
|
|
const before = pattern.slice(0, m.index);
|
|
const after = pattern.slice(m.index + m[0].length);
|
|
const opts = m[1].split(",");
|
|
return opts.flatMap((opt) => expandBraces(`${before}${opt}${after}`));
|
|
}
|
|
|
|
/** convert a glob pattern to a regex anchored at start + end. */
|
|
function globToRegex(pattern: string): RegExp {
|
|
const DSTAR = "\u0000DSTAR\u0000";
|
|
let s = pattern.replace(/\*\*/g, DSTAR);
|
|
s = s.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
s = s.replace(/\*/g, "[^/]*");
|
|
s = s.replace(/\?/g, "[^/]");
|
|
s = s.replaceAll(DSTAR, ".*");
|
|
return new RegExp(`^${s}$`);
|
|
}
|
|
|
|
/** does any path in `paths` match any glob in `patterns`? */
|
|
export function anyMatch(paths: string[], patterns: string[]): boolean {
|
|
if (patterns.length === 0) return false;
|
|
const regexes = patterns.flatMap((p) => expandBraces(p)).map(globToRegex);
|
|
return paths.some((path) => regexes.some((r) => r.test(path)));
|
|
}
|
|
|
|
/**
|
|
* decide whether an entry runs given changed files + its coverage globs.
|
|
*
|
|
* three short-circuits:
|
|
* 1. `full` flag (e.g. main pushes, workflow_dispatch) → always run
|
|
* 2. any changed file matches `ALWAYS_RUN_ALL` → run everything
|
|
* 3. coverage missing or empty on the entry → run (defensive default)
|
|
*
|
|
* otherwise: run iff any changed file matches the entry's coverage globs.
|
|
*
|
|
* `coverage: []` is treated identically to `coverage: undefined` to avoid the
|
|
* footgun where a future test author intends "skip on PRs" by passing an
|
|
* empty array — silently skipping CI on every PR is worse than always running.
|
|
*/
|
|
export type ShouldRunInput = {
|
|
changedFiles: string[];
|
|
coverage: string[] | undefined;
|
|
full: boolean;
|
|
};
|
|
|
|
export function shouldRun(input: ShouldRunInput): boolean {
|
|
if (input.full) return true;
|
|
if (anyMatch(input.changedFiles, ALWAYS_RUN_ALL)) return true;
|
|
if (input.coverage === undefined || input.coverage.length === 0) return true;
|
|
return anyMatch(input.changedFiles, input.coverage);
|
|
}
|