Two lists had to name the same CI-toolchain image sources, and only prose said so:
.gitea/workflows/ci-image.yml → on.push.paths — which pushes to main publish a toolchain image.
.gitea/workflows/pr-checks.yml → ci-image-pin's expected="$(git log -1 --format=%H -- docker/ci)" — what the pin must name.
#744 removed the shared self-reference that had kept them in step (ci.toolchain-image-publish-is-a-dispatch), leaving the agreement carried by three prose comments. Divergence is silent and green in the dangerous direction: a path that publishes but is not in the pathspec produces images the pin never tracks; a path in the pathspec that does not publish reddens a blocking job forever.
What ships
scripts/tests/test_ci_image_paths_pin_agreement.py derives BOTH lists from the workflow documents and asserts set equality in both directions, with its docs/guard-inventory.md row, a declared clause mutation in scripts/tests/mutation_manifest.py, and the three prose sites repointed at the guard.
The comparison is deliberately narrow, and that is the substance. It accepts a publish entry spelled exactly <dir>/** against a pathspec entry spelled exactly <dir>, segments restricted to [A-Za-z0-9._-], and raises on every other spelling rather than deciding what that spelling would have selected. Measured against Gitea 1.27.1's compiler (gitea.com/gitea/actpkg/workflowpattern) and real git:
spelling
Gitea compiles to
selects
docker/ci/**
^docker/ci/.*\Z
the subtree, except a newline-named descendant
docker/ci
^docker/ci\Z
nothing under the directory — while the pathspec docker/ci selects all of it
<file>/**
anchored literal
nothing — while the pathspec <file> tracks the file
A canonicaliser mapping the two dialects onto one string form was built twice and defeated twice, each repair surfacing another spelling — the trigger testing.verification-code-needs-its-own-proof names for stop-and-subtract — so it was deleted rather than extended.
The guard also asserts from the git index that each named path really is a directory (<file>/** and the pathspec <file> spell the same string); takes the pathspec from the git logassignment rather than any git log in the job, so an inert one in a comment or heredoc is not adopted; and refuses a << token on a code line (a herestring excluded, since pr-checks.yml already uses one) and a second bare --, because telling a path separator from an option argument needs git's option arity and refusing needs nothing.
The docstring states two boundaries rather than implying coverage: the guard compares the pathspec the pin job writes and does not establish that the staleness comparison consumes it; and a descendant whose name contains a newline is matched by the git pathspec but not by the publish pattern.
Verification
21 tests in the new file; full scripts/tests green (1433 passed, 2 skipped).
Every clause witnessed red by disarming it individually under python3 -B -p no:cacheprovider — two mutants of the same clause produce same-size files within one mtime tick, and stale bytecode once attributed a red to the wrong mutant.
The declared mutation is executed by test_mutation_harness.py every run: disarming published - required reddens only publish-path-added; disarming required - published reddens only pathspec-extra-entry. Each clause is separately load-bearing, so CLAUSE is honest.
The advertised property executed: git mv docker/ci docker/toolchain plus both workflow lists, with no edit to the guard, leaves the file green; a one-sided edit reddens only the agreement test.
Eight independent cold-review rounds (Codex and a different Claude line, each from a cold review-only brief in an isolated worktree). No round found a false green. Every finding was reproduced locally before fixing and pinned as a regression test where it had one.
Done-when
A guard asserts the two lists agree, deriving both from the workflow files rather than restating either
It ships a declared clause mutation re-run every suite, and a docs/guard-inventory.md row
The three prose comments that currently carry this invariant point at the guard
Adversarial review passed
fixes #855
## The gap
Two lists had to name the same CI-toolchain image sources, and only prose said so:
- `.gitea/workflows/ci-image.yml` → `on.push.paths` — which pushes to `main` publish a toolchain image.
- `.gitea/workflows/pr-checks.yml` → `ci-image-pin`'s `expected="$(git log -1 --format=%H -- docker/ci)"` — what the pin must name.
#744 removed the shared self-reference that had kept them in step (`ci.toolchain-image-publish-is-a-dispatch`), leaving the agreement carried by three prose comments. Divergence is silent and green in the dangerous direction: a path that publishes but is not in the pathspec produces images the pin never tracks; a path in the pathspec that does not publish reddens a blocking job forever.
## What ships
`scripts/tests/test_ci_image_paths_pin_agreement.py` derives BOTH lists from the workflow documents and asserts set equality in both directions, with its `docs/guard-inventory.md` row, a declared clause mutation in `scripts/tests/mutation_manifest.py`, and the three prose sites repointed at the guard.
**The comparison is deliberately narrow, and that is the substance.** It accepts a publish entry spelled exactly `<dir>/**` against a pathspec entry spelled exactly `<dir>`, segments restricted to `[A-Za-z0-9._-]`, and raises on every other spelling rather than deciding what that spelling would have selected. Measured against Gitea 1.27.1's compiler (`gitea.com/gitea/act` `pkg/workflowpattern`) and real git:
| spelling | Gitea compiles to | selects |
|---|---|---|
| `docker/ci/**` | `^docker/ci/.*\Z` | the subtree, except a newline-named descendant |
| `docker/ci` | `^docker/ci\Z` | **nothing under the directory** — while the pathspec `docker/ci` selects all of it |
| `<file>/**` | anchored literal | nothing — while the pathspec `<file>` tracks the file |
| `/docker/ci/**` | anchored literal | nothing — and `git log -- /docker/ci` aborts, `fatal: Invalid path` |
A canonicaliser mapping the two dialects onto one string form was built twice and defeated twice, each repair surfacing another spelling — the trigger `testing.verification-code-needs-its-own-proof` names for stop-and-subtract — so it was deleted rather than extended.
The guard also asserts from the git index that each named path really is a directory (`<file>/**` and the pathspec `<file>` spell the same string); takes the pathspec from the `git log` **assignment** rather than any `git log` in the job, so an inert one in a comment or heredoc is not adopted; and refuses a `<<` token on a code line (a herestring excluded, since `pr-checks.yml` already uses one) and a second bare `--`, because telling a path separator from an option argument needs git's option arity and refusing needs nothing.
The docstring states two boundaries rather than implying coverage: the guard compares the pathspec the pin job *writes* and does not establish that the staleness comparison consumes it; and a descendant whose name contains a newline is matched by the git pathspec but not by the publish pattern.
## Verification
- 21 tests in the new file; full `scripts/tests` green (1433 passed, 2 skipped).
- Every clause witnessed red by disarming it individually under `python3 -B -p no:cacheprovider` — two mutants of the same clause produce same-size files within one mtime tick, and stale bytecode once attributed a red to the wrong mutant.
- The declared mutation is executed by `test_mutation_harness.py` every run: disarming `published - required` reddens only `publish-path-added`; disarming `required - published` reddens only `pathspec-extra-entry`. Each clause is separately load-bearing, so `CLAUSE` is honest.
- The advertised property executed: `git mv docker/ci docker/toolchain` plus both workflow lists, with no edit to the guard, leaves the file green; a one-sided edit reddens only the agreement test.
- Eight independent cold-review rounds (Codex and a different Claude line, each from a cold review-only brief in an isolated worktree). No round found a false green. Every finding was reproduced locally before fixing and pinned as a regression test where it had one.
## Done-when
- [x] A guard asserts the two lists agree, deriving both from the workflow files rather than restating either
- [x] It ships a declared clause mutation re-run every suite, and a `docs/guard-inventory.md` row
- [x] The three prose comments that currently carry this invariant point at the guard
- [x] Adversarial review passed
`ci-image.yml`'s `on.push.paths` decides which pushes to `main` publish a toolchain image;
`ci-image-pin`'s `git log` pathspec decides what the pin must name. #744 removed the shared
self-reference that kept them in step, leaving the agreement carried by three prose comments, and
divergence is silent and green in the dangerous direction.
The guard derives both lists from the workflow documents and compares them for set equality in both
directions. The comparison is deliberately narrow: it accepts a publish entry spelled exactly
`<dir>/**` against a pathspec entry spelled exactly `<dir>`, segments restricted to
`[A-Za-z0-9._-]`, and raises on every other spelling rather than deciding what that spelling would
have selected.
That narrowness is the substance. Measured against Gitea 1.27.1's compiler and real git, a bare
`docker/ci` in `paths:` compiles to an anchored `^docker/ci` and selects none of the directory's
contents while the git pathspec `docker/ci` selects all of them; `<file>/**` matches nothing while
the pathspec `<file>` tracks the file; a leading `/` is literal to Gitea while git refuses it
outright. A canonicaliser mapping the two dialects onto one string form was built twice and defeated
twice, each repair surfacing another spelling, so it was deleted rather than extended per
`testing.verification-code-needs-its-own-proof`.
The guard also asserts from the git index that each named path really is a directory, since
`<file>/**` and the pathspec `<file>` spell the same string; takes the pathspec from the `git log`
assignment rather than any `git log` in the job; and refuses a `<<` token on a code line (a
herestring excluded) and a second bare `--`, because telling a path separator from an option
argument needs git's option arity and refusing needs nothing.
The docstring states the boundaries rather than implying coverage: the guard compares the pathspec
the pin job writes and does not establish that the staleness comparison consumes it, and a descendant
whose name contains a newline is matched by the git pathspec but not by the publish pattern.
fixes#855
Nine cold review rounds, no false green found in any. Round 9 fuzzed 27720 pairs against a port of Gitea 1.27.1 own compiler and returned all severity bands empty on this head. Full scripts/tests green 1435 passed 2 skipped.
Review-verdict: MERGEABLE @ 5e15dd1
Nine cold review rounds, no false green found in any. Round 9 fuzzed 27720 pairs against a port of Gitea 1.27.1 own compiler and returned all severity bands empty on this head. Full scripts/tests green 1435 passed 2 skipped.
timothy
merged commit dd0f75f1b6 into main2026-08-30 21:25:24 +02:00
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.
fixes #855
The gap
Two lists had to name the same CI-toolchain image sources, and only prose said so:
.gitea/workflows/ci-image.yml→on.push.paths— which pushes tomainpublish a toolchain image..gitea/workflows/pr-checks.yml→ci-image-pin'sexpected="$(git log -1 --format=%H -- docker/ci)"— what the pin must name.#744 removed the shared self-reference that had kept them in step (
ci.toolchain-image-publish-is-a-dispatch), leaving the agreement carried by three prose comments. Divergence is silent and green in the dangerous direction: a path that publishes but is not in the pathspec produces images the pin never tracks; a path in the pathspec that does not publish reddens a blocking job forever.What ships
scripts/tests/test_ci_image_paths_pin_agreement.pyderives BOTH lists from the workflow documents and asserts set equality in both directions, with itsdocs/guard-inventory.mdrow, a declared clause mutation inscripts/tests/mutation_manifest.py, and the three prose sites repointed at the guard.The comparison is deliberately narrow, and that is the substance. It accepts a publish entry spelled exactly
<dir>/**against a pathspec entry spelled exactly<dir>, segments restricted to[A-Za-z0-9._-], and raises on every other spelling rather than deciding what that spelling would have selected. Measured against Gitea 1.27.1's compiler (gitea.com/gitea/actpkg/workflowpattern) and real git:docker/ci/**^docker/ci/.*\Zdocker/ci^docker/ci\Zdocker/ciselects all of it<file>/**<file>tracks the file/docker/ci/**git log -- /docker/ciaborts,fatal: Invalid pathA canonicaliser mapping the two dialects onto one string form was built twice and defeated twice, each repair surfacing another spelling — the trigger
testing.verification-code-needs-its-own-proofnames for stop-and-subtract — so it was deleted rather than extended.The guard also asserts from the git index that each named path really is a directory (
<file>/**and the pathspec<file>spell the same string); takes the pathspec from thegit logassignment rather than anygit login the job, so an inert one in a comment or heredoc is not adopted; and refuses a<<token on a code line (a herestring excluded, sincepr-checks.ymlalready uses one) and a second bare--, because telling a path separator from an option argument needs git's option arity and refusing needs nothing.The docstring states two boundaries rather than implying coverage: the guard compares the pathspec the pin job writes and does not establish that the staleness comparison consumes it; and a descendant whose name contains a newline is matched by the git pathspec but not by the publish pattern.
Verification
scripts/testsgreen (1433 passed, 2 skipped).python3 -B -p no:cacheprovider— two mutants of the same clause produce same-size files within one mtime tick, and stale bytecode once attributed a red to the wrong mutant.test_mutation_harness.pyevery run: disarmingpublished - requiredreddens onlypublish-path-added; disarmingrequired - publishedreddens onlypathspec-extra-entry. Each clause is separately load-bearing, soCLAUSEis honest.git mv docker/ci docker/toolchainplus both workflow lists, with no edit to the guard, leaves the file green; a one-sided edit reddens only the agreement test.Done-when
docs/guard-inventory.mdrowReview-verdict: MERGEABLE @
5e15dd1Nine cold review rounds, no false green found in any. Round 9 fuzzed 27720 pairs against a port of Gitea 1.27.1 own compiler and returned all severity bands empty on this head. Full scripts/tests green 1435 passed 2 skipped.