Research-2080 — required-workflow path-filter closure¶
- Status: Completed
- Date: 2026-09-24
- Author: lusoris
- Governing issue: canonical
BUG-098/T-PATH-FILTERS-WEAKEN-NEW-GATES-2026-09-22 - Governing ADRs: ADR-1297 (every reporting check is required), ADR-0313 (required-check aggregator), ADR-1140 (CI impact planner)
- ADR status: no new ADR; this restores the already-decided required-check and fail-closed impact-routing contracts
1. Problem statement¶
The required-check aggregator accepts a required context that never reports as not applicable. That is necessary for workflows that genuinely do not apply, but it made workflow-level paths: and paths-ignore: filters a silent authorization boundary: one missing glob could suppress a required check completely.
The first confirmed instance was Rust. vmafx-sys binds core/include/libvmaf/libvmaf.h, while rust-ci.yml did not start for public C header changes. Adding that one missing glob closed the known symptom but left the unsafe mechanism in place.
The repository already had a test intended to ban workflow-level filters from every workflow that hosts a required context. Its regex was:
Without multiline mode, ^ matched only the beginning of the complete YAML string. Since a workflow starts with name:, the assertion could not observe any nested paths: key and passed falsely.
Adding (?m) was the red-cap check. It identified exactly seven required-context workflows with trigger filters:
build.ymldev-container-build.ymldocker-image.ymldoxygen-public-api.ymlffmpeg-integration.ymlhelm-chart.ymlrust-ci.yml
Together these workflows publish twelve required contexts. All twelve could previously be absent rather than green or red.
2. Required behavior¶
Every required-context workflow now uses the same three-stage contract:
- An unconditional
impactjob checks out full history and runsscripts/ci/plan-ci-impact.py. - A distinctly named
... workjob runs the expensive build only when the selected impact output istrueand the pull request is not a draft. - An
if: always()gate job owns the exact required context name and accepts only:selected=true/work=successorselected=false/work=skipped.
The gate fails if planning failed, if selected work failed or was cancelled, or if the planner and work job disagree. Therefore a documentation-only change still emits a cheap successful context, while a routing or execution failure cannot become a silent absence.
All twelve gate names also live in the aggregator's strictMustReport set. Each workflow now declares both pull-request and master-push triggers, so a missing gate is a broken registration rather than a legitimate not-applicable result.
GitHub does not create a dependent job's check run when the workflow starts. It creates that check only after every job in needs completes. The aggregator's two-minute missing-registration grace therefore cannot judge the exact-name gates directly while 30-75 minute work jobs are running. It now maps each delayed gate to its planner/work check names, keeps polling while one is active, and allows a bounded two-minute propagation window after completion. It also reads every Checks API page; the conversion adds enough planner/work/gate checks that a full run can exceed the 100-item page size.
The routing is explicit:
| Workflow | Selector | Required contexts |
|---|---|---|
build.yml | c_core | Linux Intel LLVM, macOS Clang+Metal, Windows MSVC+CUDA (full) |
dev-container-build.yml | dev_container | Dev Container Build |
docker-image.yml | docker_image | Docker Image Build |
doxygen-public-api.yml | doxygen | Doxygen Public API |
ffmpeg-integration.yml | c_core | FFmpeg Ubuntu gcc, FFmpeg macOS clang, FFmpeg SYCL |
helm-chart.yml | helm | helm lint + template |
rust-ci.yml | rust | vmafx-sys CI, cargo-deny |
The seven workflow files are themselves full_patterns in .github/ci-impact.json, so an edit to any routing contract runs every selector. New docker_image, dev_container, doxygen, and helm selectors preserve the old narrow execution scope without preserving the trigger-level bypass.
Matrix work uses a shared aggregate result. Consequently one failed Build matrix row fails all three Build gates, and one failed ordinary FFmpeg matrix row fails both of its gates. This is stricter than reporting a sibling green while the shared required consumer contract is broken and avoids check-name masking.
3. Alternatives considered¶
| Alternative | Benefit | Failure mode / cost | Verdict |
|---|---|---|---|
| Keep workflow filters and add the currently missing globs | Small diff; no extra planner jobs | Every future dependency edge must be duplicated perfectly in trigger YAML; another omission again becomes a silent pass | Rejected |
| Remove filters and run every heavy job on every pull request | Simplest correctness model | Needlessly schedules CUDA, SYCL, FFmpeg, containers, Rust, Doxygen, and Helm for documentation-only changes | Rejected |
| Always start; route work internally; always emit fail-closed gates | Required contexts always exist while expensive work remains impact-scoped | Adds small planner/gate jobs and explicit workflow structure | Selected |
| Treat missing required contexts as aggregator failures | Eliminates this bypass centrally | Breaks intentionally non-applicable contexts across the existing aggregator model and requires a broader policy migration | Deferred beyond this bug fix |
4. Executable evidence¶
- Red-cap: after fixing the multiline anchor, the workflow contract test found the seven files listed above instead of passing falsely.
- Live GitHub evidence: in run
35953701621, dependent checkShellCheck + shfmtwas not created untilPre-Commitcompleted; itscreated_atwas2026-09-24T04:04:56Z, over five minutes after the workflow started at03:59:06Z. That falsifies the assumption that anif: always()gate is visible during itsneedswork. python3 -m unittest scripts.ci.tests.test_hiss_replay_contract scripts.ci.tests.test_ci_impact scripts.ci.tests.test_rust_ci_workflow_contract -vpasses 45 tests, including exact selector routes, planner/work/gate structure, all 576 planner/selector/work states across the twelve gates, master-push coverage, strict must-report membership, delayed registration, and check-run pagination.actionlintpasses all eight modified workflows.- PyYAML parses all eight modified workflows.
bash scripts/ci/check-aggregator-names.shreports all 79 configured required checks exactly once; matrix work names are distinct from their gate names.python3 scripts/ci/test_ffmpeg_patch_workflow_contract.pypasses 13 tests against the renamed FFmpeg work jobs and their existing warning-clean build contract.
Hosted CI and post-merge verification remain required before BUG-098 is marked closed on master.