Derive the CI toolchain population from workflow-owned metadata, not TOOLCHAIN_JOBS #789
Closed
opened 2026-08-13 22:21:30 +02:00 by timothy
·
3 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#789
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Raised by the Codex cold review of #774.
scripts/tests/test_ci_image_pin_population.pyanchors on a hand-writtenTOOLCHAIN_JOBSregistry, cross-checked both ways against the parsed workflow. That was chosen because set equality between two DERIVED sets is blind to a job losing itscontainer:block — both sides shrink together — so the population needs an anchor that does not move.The reviewer's objection stands anyway:
TOOLCHAIN_JOBSenumerates individual jobs by hand, and calling it scope rather than population does not change that. The partition test proves every job is classified; it cannot prove the classification is correct.Concrete failure it does not catch: move a .NET-dependent step into
buildorscanwhile leaving that job inBARE_RUNNER_JOBS. Every test stays green while the job now needs the toolchain image and does not have it.Proposed: an explicit machine-readable execution-class marker on each job in the workflow itself, with both sets derived from the parsed YAML, unknown/missing markers rejected, and the marker-derived toolchain set compared against actual
container.imagevalues.Sequencing hazard: editing
docker-build.ymlre-pointsci-image-pin'sexpectedat the editing commit, so this needs the publish-then-pin two-step (docs/ci-cd.md→ CI toolchain image). Same trap that kept #744 out of a bundle.Done-when
TOOLCHAIN_JOBS/BARE_RUNNER_JOBSremovedClaiming for this session (Claude Code / Opus 5, orchestrator).
Bundling #786 + #789 — they are one mechanism, and working #786 alone would build the artifact #789 removes:
TOOLCHAIN_JOBSuses".TOOLCHAIN_JOBSis itself the defect and must be deleted in favour of a machine-readable per-job marker declared in the workflow, with both sets derived from the parsed YAML.So the bundle resolves the tension in one direction: one per-job execution-class declaration owned by the workflow, both populations derived from it, unknown/missing marker a hard failure. Registry-shaped hand lists go away rather than multiply.
Progress and any scope cuts will be recorded here.
PR #872 is open, covering this and its sibling — the two were bundled because working either alone
would have built the artifact the other removes.
Review: four cold adversarial rounds, alternating model families (Claude / Codex gpt-5.6 /
both in parallel / Claude), each from a review-only brief in an isolated worktree. Severity
converged BLOCKER/HIGH → MEDIUM → LOW/NIT; the final confirmation pass verified the substance clean
(detector-extraction equivalence over 4000 randomized cases per detector, zero mismatches) and its
one blocking finding had already been fixed a commit before it reported.
The recurring defect class was prose drifting from code — over half of all findings, including a
mechanism claim in the decision record that execution refuted. Every factual claim in the changed
docs and comments has since been re-verified by execution.
Two corrections to the issues themselves, both measured rather than assumed, are recorded in the
closing records.
Awaiting CI on
d3f470f, then the H10 verdict and the merge gate.Closing record
Outcome: Closed by PR #872.
TOOLCHAIN_JOBSandBARE_RUNNER_JOBSare deleted fromscripts/tests/test_ci_image_pin_population.py. Eachdocker-build.ymljob declaresenv.CI_EXECUTION_CLASS(toolchain/bare-runner); both sets derive from it, and a missing orunrecognised marker is a hard failure.
Root cause: the literal was not carelessness — set equality between two DERIVED sets is blind to
a member leaving both at once, so a job losing its
container:block leaves the declared and pinnedsets balanced. The population needed an anchor that does not move with the block, and in 2026-08 a
reviewed list was the only one available. The workflow can hold that anchor itself.
Decisions/conventions changed:
testing.workflow-declares-its-own-job-metadata(shared with#786).
Reusable knowledge:
ADJACENCY and must be paid, not argued away. A literal three directories away survives a careless
workflow edit; a marker four lines from the
container:block does not. So the change ships aTHIRD derivation — the job's own step bodies scanned for tools present only in the CI image —
independent of both marker and block, and it is the only one of the three that can see this issue's
concrete complaint (a .NET step MOVED into a bare-runner job, where no set changes at all).
marker AND move the invocation behind a script, and all three checks go blind. What bounds it is
the failure MODE — the job then dies on a missing binary, loudly — where #774's original defect
sent a REQUIRED check green on the bare runner. A silent pass traded for a noisy crash.
docker-build.ymlre-points
ci-image-pin'sexpected, requiring a publish-then-pin two-step. The line isexpected="$(git log -1 --format=%H -- docker/ci)"— the pathspec isdocker/cionly. Verified:docker-build.ymlwas last touched bymain's HEAD andexpectedstill resolved to32747a0,matching the pin. No two-step was needed. (Since #744 the same is true of
ci-image.yml.)matched inside comments,
echostrings and heredocs (false positives — a bare-runner job whoseCOMMENT mentions dotnet would redden a correct tree) and missed
dotnet-efandplaywright(falsenegatives). Full-line comments are now stripped and the alternation is sorted longest-first; the
remaining residue is stated in the code, and is tolerable only because its direction is a loud red
with an obvious remedy, never a silent pass.
Verification:
scripts/tests1185 passed / 2 skipped on the final rebased tree. Three real mutations ofdocker-build.ymlwitnessed red (container block removed, marker removed,
dotnetstep planted in a bare-runner job);the declared clause in
mutation_manifest.pyretargeted to the surviving comparison and re-run bythe harness. Two cold adversarial reviews; first returned BLOCKED with four findings, all fixed.
Deferred: none.
Docs updated:
docs/ci-cd.md("Per-job declarations"),docs/guard-inventory.md,docs/README.md, new decision record + regenerated catalog.