Commit 74db6fa
feat: packaged CI gate — JUnit XML output + composite GitHub Action (#37)
* feat(reports): 1/3 — add reports_junit.py disk-driven JUnit XML writer
New src/coder_eval/reports_junit.py: generate_junit_xml(run_dir) reads the
run.json spine (RunSummary), optional */*/suite.json gates (SuiteRollup), and
per-failed-row task.json (best-effort plain-dict) to build a JUnit XML string;
write_junit_xml is the thin persist wrapper. Counts always equal emitted
children; status classification goes through FinalStatus.category; all
agent-derived text is scrubbed of illegal XML 1.0 chars. Production code never
parses XML. Adds defusedxml (dev-only) for test-side round-trip parsing and a
shared write_run_json conftest fixture reused by later phases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): 2/3 — wire run --junit-xml and report -f junit
Add --junit-xml to `coder-eval run` (writes the JUnit report after the run
summary is persisted and before the failure exit-code gate, so a red run still
produces a report; write errors propagate). Add 'junit' to `coder-eval report
--format` (regenerates from any run dir; defaults to <run-dir>/junit.xml, -o
overrides; missing run.json → clean red error). Output-only — does not enter the
config merge. Docs: CI-pipeline tutorial JUnit section + CLAUDE.md tree entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ci): 3/3 — publish composite action, release automation, PR dogfood
Add action.yml at the repo root: a composite action that installs a pinned
coder-eval (or the local checkout via `version: local`), runs the suite, always
emits run-dir/junit-path outputs and a run.md job summary, then exits with
coder-eval's real exit code. Inputs cross into bash via env: only.
release.yml now bumps the action's `version:` default inside the amended release
commit (anchored on a `# <-- kept in sync` comment, with a loud grep guard) and
force-moves the `v<major>` tag after pushing the release tag.
pr-checks.yml gains a fork-gated `action-dogfood` job that exercises the action
via `uses: ./` on one Haiku task and verifies the JUnit + run.json output — the
sensor for this surface, since it isn't covered by pytest.
The action is deliberately agent-agnostic: it installs no coding-agent runtime,
so callers provide the Claude CLI (as the dogfood job does). Documented in the
README and the CI tutorial along with the pull_request_target security caveat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps): lock defusedxml (dev-only, test-side XML parsing)
Companion to the pyproject dev-group addition in 1/3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reports): code review fixes for the JUnit CI gate
Harden reports_junit.py against schema-skewed / crafted run.json rows, which are
untyped dicts (RunSummary.task_results is list[dict[str, Any]]) and may be
blob-pulled from elsewhere:
- Path containment: a crafted variant_id/task_id could steer the task.json
lookup outside the run dir (absolute value discards run_dir, ".." walks up).
Reject unsafe path components and verify containment after resolve().
- UnicodeDecodeError is a ValueError but NOT a json.JSONDecodeError, so a
task.json with undecodable bytes aborted the whole report instead of falling
back. Catch ValueError.
- Ambiguous replicate: with replicate_index absent and several replicate dirs,
the writer attributed replicate 00's failure detail to the row. Degrade to
the status-only body instead of misattributing.
- bool is an int subclass, so a bool replicate_index rendered as "[01]" and a
bool duration as a time. Exclude bool explicitly.
- Guard non-finite/negative durations (NaN would emit an invalid time="nan").
- Coerce non-str variant_id/task_path rather than crashing on dict keys/Path().
- Skipped testcases now use the suffix-stripped path, not the bare stem, so two
skipped tasks sharing a basename keep distinct JUnit identities.
- A row with no status reads as "<missing>" rather than "None".
Found by the final multi-model review (gpt-5.6, gemini-3.1-pro, gpt-5.3-codex);
each fix has a regression test that fails without it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reports): make skipped-task JUnit names platform-independent
_skipped_suite built the testcase name with str(Path(path).with_suffix("")),
which yields "tasks\opt" on Windows and "tasks/opt" on Linux. That made the
JUnit testcase identity depend on the OS that generated the report, so the same
logical run would split into two identities in CI history/flake tracking (and it
broke the Windows smoke job).
Normalize separators and parse with PurePosixPath so the emitted name is always
"/"-separated. Adds a regression test feeding a Windows-style path.
Also switch the action's model in the README/tutorial examples from Haiku to
Sonnet (docs only; the dogfood job stays on Haiku for per-PR cost).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: re-trigger CI (GitHub dropped the force-push event)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(action): generic env passthrough + minimum-task-score gate
Replace the single `anthropic-api-key` input with a generic `env`
NAME=VALUE passthrough (exported for the run step only, never
$GITHUB_ENV) and add an optional `minimum-task-score` floor read from
the always-written run.json spine. Both default off, so existing
behavior is unchanged. README, tutorial, and the dogfood job route
ANTHROPIC_API_KEY through `env`.
Code-review hardening (gemini-3.1-pro + gpt-5.3-codex + opus):
- score gate skips non-finite weighted_score (json.loads parses NaN;
NaN makes min()/>= order-dependent and could mask a below-floor task)
- minimum-task-score validated (numeric, finite, in [0.0, 1.0]) with a
clean ::error:: instead of a raw traceback
- malformed env entries reported by position, never echoing the raw
line, so a mis-prefixed secret can't leak into logs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reports): JUnit CI-gate review fixes + CE027 env-var lint
Code-review fixes to the JUnit XML CI gate, plus a new doc/config lint
rule and a producer→consumer parity test.
reports_junit.py:
- Load task.json for dataset rows: task_id "<suite>/<row_id>" is a real
nested dir, so validate it as a contained relative path (_is_safe_relpath)
instead of rejecting any '/'. Rejects absolute/backslash/'..'/Windows-drive
values; the resolve()-containment check remains the backstop.
- Guard the root <testsuites> time against NaN/inf like per-testcase time
via a shared _time_attr helper; also degrade (not crash) on a
pathologically large integer duration that overflows float()/'.3f'.
- Render informational (gating=False) criteria as [INFO], not [FAIL]/[PASS],
mirroring reports.py; only an explicit JSON `false` is informational
(null / non-bool fails safe to gating).
Docs: fix CODER_EVAL_API_BACKEND -> API_BACKEND (the real Settings env
name; the CODER_EVAL_-prefixed spelling was silently dropped by
Settings extra="ignore", selecting no backend).
CE027 (tests/lint/doc_env_parity.py): new rule flagging framework-prefixed
env-var assignments in README/action.yml/docs that no Settings field/alias
or src/ consumer backs — the class that produced the API_BACKEND bug.
Assignment-scoped with a hardened boundary and a real-consumer src scan to
avoid false positives.
Tests: dataset nested-load, nested '..'/drive rejection, informational
[INFO] (passing + null-gating), root/huge-int time degradation, and a
producer→consumer parity test running real eval_result_to_task_dict output
through generate_junit_xml.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 793355b commit 74db6fa
17 files changed
Lines changed: 2162 additions & 9 deletions
File tree
- .github/workflows
- docs/tutorials
- src/coder_eval
- cli
- tests
- lint
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
831 | 831 | | |
832 | 832 | | |
833 | 833 | | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
| 185 | + | |
186 | 186 | | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
187 | 191 | | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
188 | 204 | | |
189 | | - | |
190 | | - | |
191 | | - | |
192 | | - | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
193 | 208 | | |
194 | 209 | | |
195 | | - | |
| 210 | + | |
196 | 211 | | |
197 | 212 | | |
198 | 213 | | |
199 | 214 | | |
200 | 215 | | |
201 | 216 | | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
202 | 229 | | |
203 | 230 | | |
204 | 231 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
120 | 121 | | |
121 | 122 | | |
122 | 123 | | |
| 124 | + | |
123 | 125 | | |
124 | 126 | | |
125 | 127 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
99 | 167 | | |
100 | 168 | | |
101 | 169 | | |
| |||
0 commit comments