Skip to main content

A workflow's paths: filter and what the job actually depends on drift apart, silently

IMPLEMENTATION RULES: Before implementing this plan, read and follow:

Status: Backlog​

Goal: Decide how a workflow's declared paths: can be kept honest against what its job really reads, so this stops being found one instance at a time by the tester.

Last Updated: 2026-08-30

Priority: Medium. Nothing is broken right now — all four known instances are fixed. It earns an investigation because the class has recurred four times and been fixed four times without ever being addressed.

Problem Summary​

A paths: filter is a claim: these are the files that make this job's answer change. When the claim is narrower than the truth, the job does not run, and a not-run job is indistinguishable from a passing one in every UI that matters. No red mark, no annotation, no failure — just silence.

Four instances, all in this repository, none found by the author:

WorkflowWhat the filter missedConsequenceFound by
1test-uis.ymlansible/**A playbook change ran no tests at all — how a hardcoded postgres pod name shipped and stayed greentester
2docs.ymlversion.txtThe navbar badge read version.txt at build time, so a version bump never rebuilt the site and the badge sat a release behind — showed v1.6.0 while main was v1.6.1tester
3generate-uis-docs.ymlits own workflow fileThe 2026-08-30 runtime bump edited this workflow and the workflow did not notice, so its bumped action pins merged and never executedtester
4test-uis.ymlits own workflow file, in both triggersSame as 3. Found by audit while fixing 3, not by anyone hitting itaudit

Instance 4 is the one that turns this from a habit into a finding: a check that takes under a second found a live instance nobody had hit yet. Two of the four workflows were wrong at the moment someone finally looked.

⚠️ docs.yml already carries a comment recording that this was the second time. The scar tissue exists; the check does not.

Two problems, not one​

They want separating, because one is trivially checkable and the other is not.

A. Does a workflow list its own file? Purely structural. A workflow that cannot be triggered by editing itself cannot test its own change, and the only remedy is a human who does not know they are needed. Terje's framing, 2026-08-30: CI should be automatic. This is decidable by reading the YAML alone, needs no knowledge of what the job does, and would have caught instances 3 and 4.

B. Does the filter cover what the job actually reads? Instances 1 and 2 were dependencies outside the filtered tree — a playbook directory, and a version file read at build time. There is no general way to know what a job reads without running it, so any check here is a heuristic. Some tractable sub-cases:

  • A step that reads a path (cat version.txt, bash provision-host/uis/tests/run-tests.sh) which no paths: entry covers.
  • A workflow that runs a script, where the script's inputs are outside the filter — instance 1's exact shape, since the static suite scans ansible/.
  • Comparing what a job touched during a real run against the filter, after the fact.

What is already known​

The static test suite in this repo has the right shape for problem A, and the precedent is deliberate. test-external-proxy-contract.sh keys off the templates on disk rather than a list of names, so adding a proxy template starts guarding that service with no edit to the test. The same trick applies: enumerate .github/workflows/*.yml, assert each lists its own path in every trigger that has a paths: block. That guard cannot go stale as workflows are added.

Two constraints that suite established and that any new guard should inherit:

  • It must carry a self-check. A matcher that silently stops matching reports PASS forever.
  • An empty subject list must fail, not pass. Finding zero workflows is a broken gate, not a clean repo.

Open Questions​

  1. Is problem A worth a guard on its own, or only alongside some progress on B? A is cheap and complete; B is open-ended. Shipping A alone risks declaring the class closed when it is not.
  2. Should a paths: filter exist at all on the cheap workflows? test-uis.yml runs a static suite in about a minute. A filter saves little and has now been wrong twice on that one file. Running always is a legitimate answer to a filter that keeps being wrong — and it is not available to build-uis-container.yml, which is a 13–20 minute multi-arch build.
  3. For B, is the tractable version "compare against what a real run touched" rather than static analysis? That needs run data nobody currently collects.
  4. Does this belong with the other CI-hygiene work, or stand alone? It is cross-cutting: every instance was in a different workflow.

Suggested Next Step​

Implement A as a static test, with the self-check and empty-list rules above, and re-run the audit that found instance 4 as its first red case. Leave B in this file until someone has a mechanism they believe in — a heuristic that produces false positives on a correctness check trains people to route around it, which is the failure mode this repository has already recorded elsewhere.

  • [[INVESTIGATE-system-launcher-image-version-drift]] — the same "two things that must agree, and nothing checks that they do" shape, one layer down