Files
ersatztv/scripts
timothyandClaude Opus 5 7fcb5e9b28 fix(767): gate the release path on the delimiter ban with a prerequisite job
The ban that keeps `build`'s `Smoke + IPTV E2E` from being silently dropped was enforced
only by a pytest in `script-tests` — `on: pull_request`, and not a required context. Nothing
re-checked it on a `v*` tag push, which is exactly when the candidate image is published and
`DeployStack jazz-media` promotes it. A delimiter that reached `main` would drop `Smoke` on
the tag build, publish an unsmoked candidate, and report green.

A `scan` job now runs the PyYAML-based ban test, and `build` lists it in `needs:`. That edge
is the whole property: a red `scan` skips `build` outright, so the image is never built.

TWO DESIGNS WERE TRIED AND THE FIRST ONE'S FAILURES ARE RECORDED, because both are easy to
re-invent. The first cut put a bespoke stdlib scanner in `build` itself, as an unconditional
step before `Build and push`. Two independent cold reviews rejected it:

  * A guard STEP cannot protect the job it lives in. `build` is what publishes, so a dropped
    guard step there fails OPEN — and "the guard's own body has no opener, so it cannot be
    dropped" is circular when the only thing enforcing that property is the same PR-only test
    being backstopped. A `needs:` edge is not circular.
  * The hand-written YAML parser had ~10 false NEGATIVES in one review round (flow mappings,
    a quoted `"run":` key, aliases, multiline quoted scalars) — strictly WEAKER than the check
    it backstopped, in the only direction that matters for a security gate. Deleted rather
    than patched: running the existing test needs no second definition of "what is a `run:`
    body", so there is no drift surface at all.

No third marker bucket was needed. The deferral assumed the answer had to be markers on
`build`, modelling `Smoke`'s publish-ref `if:`. The delimiter class is a STATIC property of
the workflow text, so a job that reads the text catches it without modelling any `if:`.

The `scan` job's own steps carry #756 markers and a trailing assert, so a drop inside it is
caught too — moving the terminal assumption rather than removing it: to fail open you must
now drop the pytest step AND the assert step.

Every way of disarming the gate was mutation-tested to a red: removing the `needs:` edge,
adding a job-level `if:`, marking a step `continue-on-error`, injecting a delimiter into a
scan body, dropping the ban test from the pytest invocation, removing a marker, and deleting
the assert step. The guard's real command line is also driven against the steps' real marker
lines with each key dropped in turn.

Refs: #767
Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 19:54:08 +02:00
..
2024-01-15 10:29:07 -06:00
2024-01-16 13:28:46 -06:00