Skip to content

Commit 02fe873

Browse files
committed
fix(ci): narrow goldens and gate supported conformance
1 parent a1d01f2 commit 02fe873

7 files changed

Lines changed: 163 additions & 77 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,24 +72,32 @@ jobs:
7272
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
7373
with:
7474
persist-credentials: false
75+
lfs: false
7576
- name: Materialize conformance goldens
7677
run: |
77-
# The only workspace tests that read goldens at test time
78-
# (`test_golden_acquirejob_payloads_parsing` and runner-watch's
79-
# scenario-07 replay) read the pinned runner version's scenarios
80-
# (~10 MB). Derive the version from versions.toml so this stays in
81-
# lockstep with those tests. The old `--include=".runner-watch/golden/**"`
82-
# also fetched the other runner corpora (hundreds of MB) that only the
83-
# conformance workflows replay, on every shard.
78+
# Only these nine captures are read by workspace tests:
79+
# eight acquirejob parser fixtures plus runner-watch's scenario-07
80+
# replay metadata. Fetch each flow directly instead of the complete
81+
# pinned corpus (which also contains 100+ MB container captures).
8482
version=$(python3 -c 'import tomllib; print(tomllib.load(open("versions.toml", "rb"))["runner_version"])')
8583
git lfs install --local
86-
git lfs pull --include=".runner-watch/golden/v${version}/**"
87-
fixture=".runner-watch/golden/v${version}/06-multi-step/flows.jsonl"
88-
test -s "$fixture"
89-
if grep -q '^version https://git-lfs.github.com/spec/' "$fixture"; then
90-
echo "LFS fixture was not materialized: $fixture" >&2
91-
exit 1
92-
fi
84+
git lfs pull --include="$(
85+
printf '.runner-watch/golden/v%s/%s/flows.jsonl,' "$version" \
86+
06-multi-step 07-step-failure 08-job-outputs-needs \
87+
10-uses-checkout 11-cache-roundtrip 12-artifact \
88+
13-composite-action 14-annotations 15-oidc-id-token
89+
)"
90+
for scenario in \
91+
06-multi-step 07-step-failure 08-job-outputs-needs \
92+
10-uses-checkout 11-cache-roundtrip 12-artifact \
93+
13-composite-action 14-annotations 15-oidc-id-token; do
94+
fixture=".runner-watch/golden/v${version}/${scenario}/flows.jsonl"
95+
test -s "$fixture"
96+
if grep -q '^version https://git-lfs.github.com/spec/' "$fixture"; then
97+
echo "LFS fixture was not materialized: $fixture" >&2
98+
exit 1
99+
fi
100+
done
93101
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
94102
with:
95103
toolchain: "1.97"

‎.github/workflows/runner-conformance.yml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,23 @@ jobs:
2828
- name: Run current runner against local Preloop
2929
run: |
3030
rm -f benchmarks/compatibility/runner/behavior/conformance-preloop.jsonl
31+
# Container and service scenarios require the VM-backed executor;
32+
# runner-light intentionally validates only host-executable fixtures.
3133
python3 benchmarks/real-world/local-runner-conformance.py \
3234
--server-binary target/debug/preloop-server \
3335
--runner-binary target/debug/preloop-runner \
3436
--client-binary target/debug/preloop-runner-client \
3537
--official benchmarks/compatibility/runner/behavior/conformance-official.jsonl \
38+
--exclude-prefix 2 \
39+
--exclude-prefix 16 \
40+
--exclude-prefix 17 \
41+
--exclude-prefix 30 \
42+
--exclude-prefix 31 \
43+
--exclude-prefix 32 \
44+
--exclude-prefix 33 \
45+
--exclude-prefix 34 \
46+
--exclude-prefix 35 \
47+
--exclude-prefix 36 \
3648
--output benchmarks/compatibility/runner/behavior/conformance-preloop.jsonl
3749
- name: Validate local runner outcomes
3850
run: |

‎Cargo.lock‎

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎benchmarks/conformance/targets.toml‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,3 @@
44
[[targets]]
55
runner_version = "2.336.0"
66
exclude_prefix = ["2"]
7-
8-
[[targets]]
9-
runner_version = "2.337.0"
10-
cell = "gh-official"
11-
scenario_prefix = "2"
12-
validate_ownership = true

‎benchmarks/real-world/local-runner-conformance.py‎

Lines changed: 102 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import json
1313
import os
1414
import secrets
15+
import shutil
1516
import signal
1617
import socket
1718
import subprocess
@@ -107,7 +108,9 @@ def submit(
107108
raise RuntimeError(f"client returned invalid submission JSON: {output!r}") from error
108109

109110

110-
def configure_runner(runner: Path, root: Path, server_url: str, token: str) -> None:
111+
def configure_runner(
112+
runner: Path, root: Path, server_url: str, token: str, name: str
113+
) -> None:
111114
env = os.environ.copy()
112115
env["PRELOOP_SYSTEM_TOKEN"] = token
113116
command_output(
@@ -121,12 +124,12 @@ def configure_runner(runner: Path, root: Path, server_url: str, token: str) -> N
121124
"--token",
122125
"t",
123126
"--name",
124-
"runner-light-local",
127+
name,
125128
"--unattended",
126129
"--replace",
127130
"--no-externals",
128131
"--labels",
129-
"self-hosted,Linux,X64",
132+
"self-hosted,mitm",
130133
],
131134
env=env,
132135
)
@@ -175,11 +178,31 @@ def scenario_workflow(scenario: Path) -> tuple[Path, int]:
175178
return workflow, int(metadata.get("duration_seconds_max", 300))
176179

177180

181+
def prepare_scenario_workspace(
182+
scenario: Path, temp_root: Path
183+
) -> tuple[Path, Path, int]:
184+
"""Expose fixture-local reusable workflows through the client convention."""
185+
workflow, duration = scenario_workflow(scenario)
186+
workflows = scenario / "workflows"
187+
if not workflows.is_dir():
188+
return workflow, scenario, duration
189+
190+
workspace = temp_root / "workspaces" / scenario.name
191+
shutil.copytree(scenario, workspace)
192+
github_workflows = workspace / ".github" / "workflows"
193+
github_workflows.mkdir(parents=True, exist_ok=True)
194+
for reusable in workflows.iterdir():
195+
if reusable.is_file():
196+
shutil.copy2(reusable, github_workflows / reusable.name)
197+
198+
return workspace / workflow.relative_to(scenario), workspace, duration
199+
200+
178201
def wait_for_run(
179202
server_url: str,
180203
token: str,
181204
run_id: str,
182-
runner: subprocess.Popen[str],
205+
runners: list[subprocess.Popen[str]],
183206
timeout_seconds: int,
184207
) -> dict[str, Any]:
185208
deadline = time.monotonic() + timeout_seconds
@@ -188,10 +211,11 @@ def wait_for_run(
188211
last = http_json(f"{server_url}/api/v1/runs/{run_id}", token)
189212
if last.get("conclusion") is not None:
190213
return last
191-
if runner.poll() is not None:
192-
raise RuntimeError(
193-
f"runner exited with {runner.returncode} before run {run_id} completed"
194-
)
214+
for runner in runners:
215+
if runner.poll() is not None:
216+
raise RuntimeError(
217+
f"runner exited with {runner.returncode} before run {run_id} completed"
218+
)
195219
time.sleep(1)
196220
raise RuntimeError(
197221
f"run {run_id} did not complete within {timeout_seconds}s; last={last}"
@@ -223,14 +247,23 @@ def official_workflows(path: Path) -> set[str]:
223247
return names
224248

225249

226-
def scenario_names(root: Path, official: Path) -> list[str]:
250+
def scenario_names(
251+
root: Path, official: Path, exclude_prefixes: list[str]
252+
) -> list[str]:
227253
import tomllib
228254

229-
names = official_workflows(official)
255+
names = {
256+
name
257+
for name in official_workflows(official)
258+
if not any(name.startswith(prefix) for prefix in exclude_prefixes)
259+
}
230260
# Include every checked-in scenario that has exactly one runnable
231261
# submit_workflow step. Other manifests are setup-only fixtures and cannot
232262
# be submitted by this harness.
233263
for manifest in root.glob("*/scenario.toml"):
264+
name = manifest.parent.name
265+
if any(name.startswith(prefix) for prefix in exclude_prefixes):
266+
continue
234267
metadata = tomllib.loads(manifest.read_text())
235268
steps = metadata.get("steps", [])
236269
submit_steps = [
@@ -239,24 +272,39 @@ def scenario_names(root: Path, official: Path) -> list[str]:
239272
if isinstance(step, dict) and step.get("kind") == "submit_workflow"
240273
]
241274
if len(submit_steps) == 1:
242-
names.add(manifest.parent.name)
275+
names.add(name)
243276
return sorted(names)
244-
245277
def main() -> int:
246278
parser = argparse.ArgumentParser(description=__doc__)
247279
parser.add_argument("--server-binary", type=Path, required=True)
248280
parser.add_argument("--runner-binary", type=Path, required=True)
249281
parser.add_argument("--client-binary", type=Path, required=True)
250-
parser.add_argument("--scenarios-root", type=Path, default=Path("experiments/mitm/scenarios"))
282+
parser.add_argument(
283+
"--scenarios-root",
284+
type=Path,
285+
default=Path("experiments/mitm/scenarios"),
286+
)
251287
parser.add_argument("--official", type=Path, required=True)
252288
parser.add_argument("--output", type=Path, required=True)
253289
parser.add_argument("--scenario", action="append", dest="scenarios")
290+
parser.add_argument(
291+
"--exclude-prefix",
292+
action="append",
293+
default=[],
294+
help="exclude discovered scenarios whose name starts with this prefix",
295+
)
254296
args = parser.parse_args()
255297

256298
if args.scenarios:
257-
scenarios = args.scenarios
299+
scenarios = [
300+
name
301+
for name in args.scenarios
302+
if not any(name.startswith(prefix) for prefix in args.exclude_prefix)
303+
]
258304
else:
259-
scenarios = scenario_names(args.scenarios_root, args.official)
305+
scenarios = scenario_names(
306+
args.scenarios_root, args.official, args.exclude_prefix
307+
)
260308
missing = [
261309
name
262310
for name in scenarios
@@ -280,9 +328,18 @@ def main() -> int:
280328
temp_root = Path(temp)
281329
server_state = temp_root / "server-state"
282330
server_log = temp_root / "server.log"
283-
runner_root = temp_root / "runner"
284-
runner_log = temp_root / "runner.log"
331+
runner_roots = [temp_root / f"runner-{index}" for index in range(4)]
332+
runner_logs = [temp_root / f"runner-{index}.log" for index in range(4)]
285333
env = os.environ.copy()
334+
# The cpane runner exports these for jobs executed inside smolVMs.
335+
# This harness runs the runner directly on the host, so inheriting the
336+
# guest bridge origin makes it try to bind cpane's fixed 9090 port.
337+
for key in (
338+
"PRELOOP_CONTROL_ORIGIN",
339+
"PRELOOP_CONTROL_SOCKET",
340+
"PRELOOP_CONTROL_UPSTREAM",
341+
):
342+
env.pop(key, None)
286343
env.update(
287344
{
288345
"PRELOOP_PUBLIC_URL": server_url,
@@ -307,29 +364,42 @@ def main() -> int:
307364
stderr=subprocess.STDOUT,
308365
text=True,
309366
)
310-
runner: subprocess.Popen[str] | None = None
367+
runners: list[subprocess.Popen[str]] = []
311368
try:
312369
wait_for_health(server_url, server, server_log)
313-
configure_runner(args.runner_binary, runner_root, server_url, token)
314-
with runner_log.open("w") as log_file:
315-
runner = subprocess.Popen(
316-
[str(args.runner_binary), "--runner-root", str(runner_root), "run"],
317-
env=env,
318-
stdout=log_file,
319-
stderr=subprocess.STDOUT,
320-
text=True,
370+
for index, (root, log_path) in enumerate(zip(runner_roots, runner_logs)):
371+
configure_runner(
372+
args.runner_binary,
373+
root,
374+
server_url,
375+
token,
376+
f"runner-light-local-{index}",
321377
)
378+
with log_path.open("w") as log_file:
379+
runners.append(
380+
subprocess.Popen(
381+
[str(args.runner_binary), "--runner-root", str(root), "run"],
382+
env=env,
383+
stdout=log_file,
384+
stderr=subprocess.STDOUT,
385+
text=True,
386+
)
387+
)
322388

323389
records: list[dict[str, Any]] = []
324390
for scenario_name in scenarios:
325391
scenario = args.scenarios_root / scenario_name
326-
workflow, duration = scenario_workflow(scenario)
327-
run_id = submit(args.client_binary, server_url, token, workflow, scenario)
392+
workflow, workspace, duration = prepare_scenario_workspace(
393+
scenario, temp_root
394+
)
395+
run_id = submit(
396+
args.client_binary, server_url, token, workflow, workspace
397+
)
328398
run = wait_for_run(
329399
server_url,
330400
token,
331401
run_id,
332-
runner,
402+
runners,
333403
max(180, duration + 60),
334404
)
335405
result = semantic_result(run)
@@ -346,12 +416,13 @@ def main() -> int:
346416
args.output.write_text("\n".join(json.dumps(record) for record in records) + "\n")
347417
except Exception:
348418
print(f"runner-light: failure artifacts at {temp_root}", file=sys.stderr)
349-
for log in (server_log, runner_log):
419+
for log in (server_log, *runner_logs):
350420
if log.exists():
351421
print(log.read_text(errors="replace"), file=sys.stderr)
352422
raise
353423
finally:
354-
stop_process(runner)
424+
for runner in runners:
425+
stop_process(runner)
355426
stop_process(server)
356427

357428
print(f"runner-light: wrote {len(scenarios)} records to {args.output}")

0 commit comments

Comments
 (0)