Files
ironclaw/scripts
Josh Ford 6605c80cc1 fix(ci): unbreak per-package clippy on bin-only crates; classify .gitignore (#7167)
* fix(ci): unbreak per-package clippy on bin-only crates; classify `.gitignore`

Two latent CI bugs, one per lane, that #6965 is the first PR to trip.

`Check production-target lints` runs `cargo clippy -p <changed package>
--lib --bins`. `--lib` is a hard error on a bin-only package, so the first
PR whose only changed package is `ironclaw` (crates/ironclaw_reborn_cli)
dies on `no library targets found in package `ironclaw`` — exit 101, before
a single lint runs. `--bins` alone is the quieter half of the same bug: on a
lib-only package cargo warns `target filter `bins` specified, but no targets
matched; this is a no-op` and the lane reports green having linted nothing.
Cargo's default target set is already lib + bins, with tests, examples, and
benches excluded, so the production-target lane needs no filter at all.

`reborn_pr_test_plan.py` had no rule for root `.gitignore`, so its
fail-closed arm raised `unclassified pull-request path: .gitignore` and
failed the whole `Tests (Reborn)` roll-up on any PR that adds an ignore
rule. It joins the decided-paths set rather than the repo-root prose set,
because something does read it: Code Style filters on it for `has_code` and
runs `Reject tracked files that match .gitignore`. That is the shape already
recorded there for `scripts/no_panics_reborn_baseline.txt` — owned by a
static check, read by no Reborn lane — and it leaves the decision in the
plan's `reasons`.

`validate_production_lint_targets` keeps the lane filter-free. It rejects
every explicit target selector, not just the two that caused the outage:
`--bin` pins a multi-bin package to one target, and `--test`/`--example`/
`--bench` and their plurals pull in what the lane exists to exclude. Flags
are matched on word boundaries so `--bins` is not also reported as `--bin`,
and `clippy_matrix` is checked too, since `${{ matrix.flags }}` expands into
the same command.

The check reads the step body rather than locating the command and parsing
its arguments: a matcher is a thing to fool, and scanning has no match
position to displace and no command formatting to get wrong. The trade —
a command deliberately written to look inert would pass — is recorded
beside the constant, along with the zero-target-package gap that is
unreachable today.

Verified red before the fix, green after: reverting the workflow to
`--lib --bins` fails the contract; removing the `.gitignore` classification
reproduces `ValueError: unclassified pull-request path: .gitignore`;
unhooking the validator from `validate_workflow_texts` fails the top-level
test. Suites: 30 workflow contracts, 44 planner, 6 shards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ci): reject edits that run the production lint and discard its verdict

From review: the contract checked which flags the lint passes but not whether
anyone reads its exit status. `|| true`, `|| :`, `set +e`, and a step-level
`continue-on-error: true` all leave the lane running clippy and ignoring the
result — the silent-green failure the whole contract exists to prevent.

These are worth catching where a disguised command is not. Each is a
plausible edit made on purpose and for a stated reason — unblock the queue,
quiet a flaky lane — rather than an attempt to fool a validator, and the
check is a substring scan over the same step body, so it adds no matcher to
bypass. The `echo cargo clippy` case raised alongside it stays out of scope
for the reason recorded beside the constant: it requires deliberate disguise
in a file that only changes through reviewed PRs, and chasing it is what
grew the previous parser through three bypasses.

Verified: removing the check fails all four sabotage cases.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 01:06:38 +00:00
..