Files
hermes-agent/tests/ci/workflow_steps.py
Teknium c52a900f13 test: the CI replay puts git on the step's PATH on native Windows too
The replayed steps call git. On Linux os.defpath reaches it; on Windows the
replay's PATH only reached it through the located bash's directory, and Git
for Windows ships a bash.exe in usr\bin with no git.exe beside it. The
Windows arm64 lane resolved that bash and 13 replay cases failed with
"git: command not found" (1e547a814c3 had just put them on the native
Windows lanes). git's own directory now rides along like bash's.
2026-10-06 13:02:14 -07:00

140 lines
6.0 KiB
Python

"""Local receipts for the few safe routing steps, NOT a hosted workflow runner.
Execute lane/selection scripts; replace only pytest/Playwright launch boundaries
with argv receipts. No setup actions, installs, E2E tests or native hosts run here.
"""
from __future__ import annotations
import fnmatch
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from hermes_platform.resolver import LookupContext, locate_command
from tests.ci import _gha_expr as gha
# Intercept launch/activation boundaries, not selection logic. The unit lane
# need not have the upgrade job's venv; the Desktop monitor must not outlive us.
_RECEIPTS = r'''
record() {
"$REPLAY_PYTHON" -c 'import json, os, sys; open(os.environ["REPLAY_CALLS"], "a").write(json.dumps(sys.argv[1:]) + "\n")' "$@"
}
source() { [[ "$1" == .venv/bin/activate ]]; }
scripts/run_tests.sh() { record pytest "$@"; }
npx() { record npx "$@"; }
xvfb-run() { shift 2; "$@"; }
sleep() { return 1; }
'''
def required(step: dict, ctx: dict) -> None:
label = step.get("name", step.get("id", "required step"))
assert gha.condition(step.get("if"), ctx), f"{label}: disabled"
assert not gha.truthy(gha.render(step.get("continue-on-error", False), ctx)), f"{label}: advisory"
def _interpreter_dirs(root: Path) -> tuple[str, ...]:
"""The replay interpreter's dir, plus shims for the names setup-python puts on PATH.
Steps call ``python3`` (Linux runners) or ``python`` (Windows runners); a
Windows venv only ships ``python.exe``, so a missing name would otherwise
hit an unrelated host interpreter on os.defpath, or none at all.
"""
interpreter = Path(sys.executable).parent
shims = root / "bin"
shims.mkdir()
for name in ("python", "python3"):
if not locate_command(name, LookupContext(path=str(interpreter))).found:
shim = shims / name
shim.write_text('#!/bin/sh\nexec "$REPLAY_PYTHON" "$@"\n', encoding="utf-8")
shim.chmod(0o755)
return str(interpreter), str(shims)
def _run(step: dict, ctx: dict, cwd: Path | None = None, *, receipt: bool = False) -> tuple[dict, list]:
required(step, ctx)
assert step.get("shell", "bash") == "bash", "only safe Bash selection steps are replayed"
with tempfile.TemporaryDirectory(prefix="workflow-replay-") as directory:
root = Path(directory)
output, calls = root / "outputs", root / "calls"
output.touch()
calls.touch()
bash = locate_command("bash").command[0]
# Steps call git too. Linux reaches it through os.defpath; on Windows the bash found
# first may be Git's usr\bin (no git.exe), so git's own dir rides along like bash's.
git = locate_command("git").command[0]
env = {
"PATH": os.pathsep.join((*_interpreter_dirs(root), os.defpath, str(Path(bash).parent),
str(Path(git).parent))),
"HOME": directory, "RUNNER_TEMP": directory, "GITHUB_OUTPUT": str(output),
"REPLAY_PYTHON": sys.executable, "REPLAY_CALLS": str(calls),
**{k: gha.to_string(gha.render(v, ctx)) for k, v in step.get("env", {}).items()},
}
script = root / "step.sh"
script.write_text((_RECEIPTS if receipt else "") + gha.render(step["run"], ctx), encoding="utf-8")
result = subprocess.run(
[bash, "--noprofile", "--norc", "-eo", "pipefail", str(script)],
cwd=cwd or root, env=env, stdin=subprocess.DEVNULL,
capture_output=True, text=True, timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
return (dict(line.split("=", 1) for line in output.read_text(encoding="utf-8-sig").splitlines()),
[json.loads(line) for line in calls.read_text(encoding="utf-8-sig").splitlines()])
def outputs(step: dict, ctx: dict, cwd: Path | None = None) -> dict:
return _run(step, ctx, cwd)[0]
def selected_files(step: dict, ctx: dict, repo: Path) -> set[str]:
"""Receipt the real command and nonempty selected files, not native execution."""
cwd = repo / step.get("working-directory", ".")
_, calls = _run(step, ctx, cwd, receipt=True)
assert len(calls) == 1, f"expected one test invocation, got {calls}"
command, *args = calls[0]
if command == "npx":
assert args[:3] == ["playwright", "test", "-c"], args
assert (cwd / args[3]).is_file(), args
paths = args[4:]
assert paths and all(p.endswith(".spec.ts") for p in paths), args
ignores = []
else:
assert command == "pytest", calls
# Only the runner options used by these steps are supported; fail closed
# when a new filter could change the selected files.
paths, ignores = [], []
i = 0
while i < len(args):
arg = args[i]
if arg == "--files":
i += 1
paths.extend(args[i].split(os.pathsep))
elif arg == "-m":
i += 1 # native marker evaluation belongs to the native runner
assert args[i] in ("platforms and integration", "platforms and not integration"), args
elif arg.startswith("--ignore-glob="):
ignores.append(arg.split("=", 1)[1])
elif arg in ("--", "--include-integration", "-v", "--tb=short", "-rA"):
pass
else:
assert arg.startswith("tests/"), f"unsupported runner argument: {arg}"
paths.append(arg)
i += 1
assert paths, "test command selected no paths"
files = set()
for rel in paths:
path = cwd / rel
assert path.exists(), f"selected test path does not exist: {path}"
candidates = path.rglob("test_*.py") if path.is_dir() else [path]
for candidate in candidates:
name = candidate.relative_to(repo).as_posix()
if not any(fnmatch.fnmatch(name, pattern) for pattern in ignores):
assert candidate.stat().st_size > 0, f"empty test file: {name}"
files.add(name)
assert files, "test command selected no files"
return files