From 0494474ea53f3b9fb59a9280f55f766f653ab670 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Thu, 6 Aug 2026 21:51:24 +0200 Subject: [PATCH 1/2] fix(ci): coverage workflow globbed a flat tests/unit that no longer exists The nightly coverage run has failed every night since 2026-08-02. Its file list came from `ls tests/unit/*_test.sh`, and #960 moved every unit test into a per-module subdirectory, so the glob matched nothing, `ls` exited non-zero and the step died under `bash -e`. Nobody noticed because this workflow is deliberately non-blocking: it never runs on push or pull_request, so a red nightly is not a status check on anything. There were two stale paths, and the second is the one that mattered. The exclusion `grep -v '/coverage_'` was written for flat filenames; the coverage engine's meta-tests are now tests/unit/coverage/. Fixing only the glob would have restored the run while silently pulling those meta-tests back in -- and the workflow's own comment explains why they are excluded: measuring them under --coverage double-instruments src/coverage/ and corrupts their assertions. Both are now one `find` with explicit exclusions for coverage/ and fixtures/. That also picks up two files a two-level glob would still have missed. 65 files selected, from 76 total minus 9 meta-tests and 2 fixtures; smoke-tested against a real --coverage run that writes lcov. The contract test is the point of this change. It existed already and did not catch this, because it only looked at tests.yml -- #972 fixed the instance and I did not go looking for the class. It now checks every workflow, for any shape: a tests/ path or glob a workflow names must resolve to something. Mutation- tested against both files. Two bugs in that test worth recording. It documented stripping comments and did not, so the broken globs the workflows describe in prose read as live references. And an unanchored `tests/` matched the tail of `sample_tests/pass_test.sh`, a file test-action.yml writes at runtime, reporting it missing. Both fixed; the match is anchored on a boundary now. --- .github/workflows/coverage.yml | 12 ++++-- tests/unit/project/ci_test_paths_test.sh | 49 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0ca654ac..2d41dcb7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -9,9 +9,9 @@ name: Coverage # so it is not a status check on any commit and cannot gate merges. Coverage is # CPU-heavy, so a scheduled cadence keeps it off the critical path. # -# The coverage engine's own meta-tests (tests/unit/coverage_*_test.sh) are -# excluded from the measured run: executing them under --coverage double- -# instruments src/coverage.sh and corrupts their assertions. +# The coverage engine's own meta-tests (tests/unit/coverage/) are excluded from +# the measured run: executing them under --coverage double-instruments +# src/coverage/ and corrupts their assertions. on: schedule: @@ -51,7 +51,11 @@ jobs: # run well under the job timeout on the multi-core runner. Parallel LCOV # aggregation is deterministic (same total as a sequential run). run: | - files=$(ls tests/unit/*_test.sh | grep -v '/coverage_') + # `find`, not a glob: unit tests live in per-module subdirectories since + # #960, so a flat glob there matches nothing and `ls` fails the step. + # fixtures/ holds inputs to other tests, never tests themselves. + files=$(find tests/unit -name '*_test.sh' \ + -not -path '*/fixtures/*' -not -path '*/coverage/*') # shellcheck disable=SC2086 ./bashunit --coverage --parallel --jobs auto \ --coverage-report coverage/lcov.info --coverage-paths src $files diff --git a/tests/unit/project/ci_test_paths_test.sh b/tests/unit/project/ci_test_paths_test.sh index e605c730..e9967ad1 100644 --- a/tests/unit/project/ci_test_paths_test.sh +++ b/tests/unit/project/ci_test_paths_test.sh @@ -79,3 +79,52 @@ EOF assert_empty "$unresolved" } + +# The same rot hit a second workflow and this file did not catch it, because it +# only ever looked at tests.yml. coverage.yml globbed `tests/unit/*_test.sh` in a +# shell step rather than a `test_path:` key, so after #960 its `ls` failed and the +# nightly coverage run had been red for days -- unnoticed, because that workflow +# is deliberately non-blocking. +# +# This checks the property across every workflow and every shape: any tests/ +# path or glob a workflow names must resolve to something on disk. +function ci_referenced_test_paths() { + # Comment lines are dropped first: the workflows document the broken globs + # they replaced, and those must not be read as live references. + # + # The match is anchored on a boundary so `sample_tests/pass_test.sh` -- a file + # test-action.yml writes at runtime -- is not read as a `tests/` path. An + # unanchored `tests/` matched its tail and reported it missing. + "$GREP" -rhv '^[[:space:]]*#' "$ROOT_DIR"/.github/workflows/ --include='*.yml' 2>/dev/null | + "$GREP" -oE '(^|[[:space:]"'"'"'=])tests/[A-Za-z0-9_*/.-]+' | + sed 's/^[^t]//' | LC_ALL=C sort -u +} + +function test_every_tests_path_named_by_any_workflow_resolves() { + local unresolved="" + local token + while IFS= read -r token; do + [ -z "$token" ] && continue + case "$token" in + */) continue ;; + esac + + local found=0 + local match + for match in $token; do + if [ -e "$match" ]; then + found=1 + break + fi + done + + if [ "$found" -eq 0 ]; then + unresolved="$unresolved$token +" + fi + done < Date: Fri, 7 Aug 2026 12:18:56 +0200 Subject: [PATCH 2/2] chore(ci): retrigger pull request checks