Architecture Finding
Type: interface-violation / tech-debt (gate scope narrower than guarded surface)
Affected area: .github/workflows/unit-tests.yml on.push.paths / on.pull_request.paths
Four of the release-critical reusable workflows are guarded by pytest drift
gates, but editing one of them alone never runs the gate that guards it,
because unit-tests.yml filters on paths: and those four files are not in the
filter.
Guarded workflow files (derived from what tests/*.py actually reads):
| workflow |
guarded by |
in unit-tests.yml paths:? |
reusable-build.yml |
test_check_consumer_contract.py, test_validate_thin_caller.py |
✅ |
reusable-renovate.yml |
test_reusable_renovate_scope.py |
✅ |
reusable-release-gate.yml |
test_release_gate_single_source.py, test_trust_policy_single_source.py, test_release_registry_normalization.py |
❌ blind |
reusable-promote-squash.yml |
test_promote_gate_order.py, test_release_registry_normalization.py |
❌ blind |
reusable-execute-release.yml |
test_trust_policy_single_source.py, test_release_registry_normalization.py |
❌ blind |
reusable-release.yml |
test_trust_policy_single_source.py |
❌ blind |
The guarded set and the trigger set are two separate hand-maintained lists with
nothing keeping them in agreement. reusable-build.yml and
reusable-renovate.yml were added to paths: when their gates landed; the four
release workflows were not.
Impact
tests/test_release_gate_single_source.py exists precisely because "nothing
invokes the scripts at runtime, so without this gate the BATS suite can stay
green while the shipped gate regresses" (module docstring). The same reasoning
applies one level up: without the trigger entry, pytest itself can stay green
— by never running — while the shipped release gate regresses.
Reproduced on main (4d3b97e). Injecting a one-character drift into the
resolve step body of reusable-release-gate.yml:
- DIGEST="$(skopeo inspect --format '{{.Digest}}' ...
+ DIGEST="$(skopeo inspect --format '{{.Digest}}' ...
makes test_inline_step_matches_extracted_script[resolve] fail locally — the
gate works. But that diff touches only .github/workflows/reusable-release-gate.yml,
which matches no entry in either paths: list, so the Unit Tests workflow is
never scheduled and the PR shows no failing check.
The blind surface is the digest-resolution / cosign-verification / promotion-
ordering logic — the supply-chain trust boundary for every consumer repo that
calls these reusables.
Recommendation
Two parts; the second is not pushable by this agent (see below).
Part 1 — machine-enforce the invariant (PR follows). Add
tests/test_unit_test_gate_scope.py, which derives the guarded set by scanning
tests/*.py for workflow filenames that exist on disk and asserts each one is
matched by unit-tests.yml's paths: filter on both push and
pull_request. This makes the trigger scope a derived consequence of the suite
rather than a parallel list: adding a gate over a new workflow forces the
trigger entry in the same change. It also asserts unit-tests.yml triggers on
itself, so narrowing the filter re-runs this gate.
Part 2 — widen the trigger filter. Apply verbatim to
.github/workflows/unit-tests.yml, in both the on.push.paths and
on.pull_request.paths lists (the two lists are identical today; keep them so):
- ".github/workflows/reusable-build.yml"
+ - ".github/workflows/reusable-execute-release.yml"
+ - ".github/workflows/reusable-promote-squash.yml"
+ - ".github/workflows/reusable-release-gate.yml"
+ - ".github/workflows/reusable-release.yml"
- ".github/workflows/reusable-renovate.yml"
- ".github/workflows/unit-tests.yml"
Verified locally: with Part 2 applied, tests/test_unit_test_gate_scope.py
passes 5/5 and the full suite is 388 passed. Without it, the full suite is
383 passed, 2 failed — and the only failures are the two new assertions
naming exactly the four blind workflows.
Why Part 2 is filed here and not in the PR
This agent runs at a token tier without the GitHub App Workflows
permission, so any push whose diff touches .github/workflows/** is rejected
server-side. Part 2 must be applied by a human maintainer or an agent with
workflow-write scope. Part 1's PR is therefore red until Part 2 lands — the
two new assertions fail by design, reporting the four unguarded workflows. They
go green the moment the diff above is applied. Please land both together.
Filed by architect agent (ACMM L5 — hold-gated mode)
— hive: agent=architect backend=copilot model=claude-opus-5
Architecture Finding
Type: interface-violation / tech-debt (gate scope narrower than guarded surface)
Affected area:
.github/workflows/unit-tests.ymlon.push.paths/on.pull_request.pathsFour of the release-critical reusable workflows are guarded by pytest drift
gates, but editing one of them alone never runs the gate that guards it,
because
unit-tests.ymlfilters onpaths:and those four files are not in thefilter.
Guarded workflow files (derived from what
tests/*.pyactually reads):unit-tests.ymlpaths:?reusable-build.ymltest_check_consumer_contract.py,test_validate_thin_caller.pyreusable-renovate.ymltest_reusable_renovate_scope.pyreusable-release-gate.ymltest_release_gate_single_source.py,test_trust_policy_single_source.py,test_release_registry_normalization.pyreusable-promote-squash.ymltest_promote_gate_order.py,test_release_registry_normalization.pyreusable-execute-release.ymltest_trust_policy_single_source.py,test_release_registry_normalization.pyreusable-release.ymltest_trust_policy_single_source.pyThe guarded set and the trigger set are two separate hand-maintained lists with
nothing keeping them in agreement.
reusable-build.ymlandreusable-renovate.ymlwere added topaths:when their gates landed; the fourrelease workflows were not.
Impact
tests/test_release_gate_single_source.pyexists precisely because "nothinginvokes the scripts at runtime, so without this gate the BATS suite can stay
green while the shipped gate regresses" (module docstring). The same reasoning
applies one level up: without the trigger entry, pytest itself can stay green
— by never running — while the shipped release gate regresses.
Reproduced on
main(4d3b97e). Injecting a one-character drift into theresolvestep body ofreusable-release-gate.yml:makes
test_inline_step_matches_extracted_script[resolve]fail locally — thegate works. But that diff touches only
.github/workflows/reusable-release-gate.yml,which matches no entry in either
paths:list, so theUnit Testsworkflow isnever scheduled and the PR shows no failing check.
The blind surface is the digest-resolution / cosign-verification / promotion-
ordering logic — the supply-chain trust boundary for every consumer repo that
calls these reusables.
Recommendation
Two parts; the second is not pushable by this agent (see below).
Part 1 — machine-enforce the invariant (PR follows). Add
tests/test_unit_test_gate_scope.py, which derives the guarded set by scanningtests/*.pyfor workflow filenames that exist on disk and asserts each one ismatched by
unit-tests.yml'spaths:filter on bothpushandpull_request. This makes the trigger scope a derived consequence of the suiterather than a parallel list: adding a gate over a new workflow forces the
trigger entry in the same change. It also asserts
unit-tests.ymltriggers onitself, so narrowing the filter re-runs this gate.
Part 2 — widen the trigger filter. Apply verbatim to
.github/workflows/unit-tests.yml, in both theon.push.pathsandon.pull_request.pathslists (the two lists are identical today; keep them so):Verified locally: with Part 2 applied,
tests/test_unit_test_gate_scope.pypasses 5/5 and the full suite is
388 passed. Without it, the full suite is383 passed, 2 failed— and the only failures are the two new assertionsnaming exactly the four blind workflows.
Why Part 2 is filed here and not in the PR
This agent runs at a token tier without the GitHub App Workflows
permission, so any push whose diff touches
.github/workflows/**is rejectedserver-side. Part 2 must be applied by a human maintainer or an agent with
workflow-write scope. Part 1's PR is therefore red until Part 2 lands — the
two new assertions fail by design, reporting the four unguarded workflows. They
go green the moment the diff above is applied. Please land both together.
Filed by architect agent (ACMM L5 — hold-gated mode)
— hive: agent=architect backend=copilot model=claude-opus-5