Summary
Since #378 made whole-tree the default, gates 19, 25 and 26 are still silently diff-scoped on every pull_request run — i.e. on exactly the event that gates a merge. They report their whole-tree backlog only on push, after the merge, where it can no longer block anything.
The cause is not the scope flag. It is that those three helpers receive the base ref through the environment, and HYDRA_GATE_BASE_REF is present in the environment on pull_request and absent on push.
Isolated control — same tree, same base commit, only the delivery channel differs
Local run of bin/hydra-gates against docudesk, base f1931874 in both arms, SCOPE-MODE: full in both:
| gate |
HYDRA_GATE_BASE_REF=f1931874 bin/hydra-gates --app-dir D |
env -u HYDRA_GATE_BASE_REF bin/hydra-gates --app-dir D --base f1931874 |
| 19 e2e-coverage |
NOT APPLICABLE |
FAIL — 396 scenario(s) |
| 25 contract-coverage |
NOT APPLICABLE |
PASS (it actually ran) |
| 26 visual-coverage |
NOT APPLICABLE |
FAIL — 6 new page component(s) |
|
COVERAGE: 56 of 65 |
COVERAGE: 58 of 65 |
Both arms print the same resolved base — only the source label changes (($HYDRA_GATE_BASE_REF) vs (--base)). Nothing else about the invocation differs.
The same split reproduces in CI as a push-vs-PR difference
| repo |
event |
base line in the log |
gate-19 |
COVERAGE |
| hermiq |
push (2 changed files) |
Delta base: 51e128e… (github.event.before (push)) |
FAIL — 491 |
60 of 65 |
| hermiq |
pull_request (4 changed files) |
Delta base: origin/development ($HYDRA_GATE_BASE_REF) |
NOT APPLICABLE |
58 of 65 |
| docudesk |
push (0 changed files) |
Delta base: 18e17b1f… (github.event.before (push)) |
FAIL — 396 |
62 of 65 |
| docudesk |
pull_request (1 changed file) |
Delta base: origin/development ($HYDRA_GATE_BASE_REF) |
NOT APPLICABLE |
58 of 65 |
On push the wrapper resolves the base from github.event.before into a shell variable and forwards it as an argument; the env var is not set, so the helper sees no base and sweeps the tree. On pull_request the caller exports HYDRA_GATE_BASE_REF, and the helper picks it up regardless of SCOPE_TO_DIFF.
Why the existing guard does not catch it
run-hydra-gates.sh gate-19 already knows about this class — its comment cites #242:
SCOPE ONLY WHEN THE CALLER ASKED FOR IT (#242). BASE_REF was passed unconditionally, so an UNSCOPED run — the mode a fleet audit uses — was silently narrowed to the diff against origin/development, came back empty, and the helper printed PASS over a repo it never opened. Measured on openconnector: 5 findings scoped, 412 over the full tree.
and the code correctly gates the explicit pass:
if [ "${SCOPE_TO_DIFF}" = "1" ]; then
HYDRA_GATE_BASE_REF="${BASE_REF}" python3 …/check_e2e_coverage.py . >> "${_e2e_log}" 2>&1
else
python3 …/check_e2e_coverage.py . >> "${_e2e_log}" 2>&1
fi
The else branch does not unset the variable. An inherited HYDRA_GATE_BASE_REF walks straight past the if into the child process, and #242 is re-opened by the environment rather than by the argument.
⚠️ This also silently sabotages local reproduction: my first attempt at reproducing docudesk's gate-19 used HYDRA_GATE_BASE_REF=… and got NOT APPLICABLE on a tree I already knew carried 396. That reads exactly like a clean repo.
Impact
#378's stated purpose is that a tightened gate should force the inherited debt to be fixed. For the three coverage gates that does not happen where it would bite:
- docudesk: 402 findings (396 + 6) invisible on every PR.
- hermiq: 502 findings (491 + 5 + 6) invisible on every PR.
And the two events produce numbers that look like progress. docudesk development push reads RESULT: 5 GATE(S) FAILED; a one-file PR against it reads RESULT: 2 GATE(S) FAILED. Nothing was fixed in between. Anyone quoting a PR gate count as a repo state — or comparing one to a push count — records a three-gate improvement that does not exist.
Suggested fix
Unset the variable in the else branch for each of the three helpers, so the environment cannot re-introduce the scoping the flag declined:
else
env -u HYDRA_GATE_BASE_REF python3 …/check_e2e_coverage.py . >> "${_e2e_log}" 2>&1
fi
⚠️ Expect the number to jump on the next PR in every repo, which is the point of #378 — but it should land deliberately rather than as a surprise on someone's unrelated one-line change.
Worth adding to the gate-acceptance fixtures as a two-arm case: same base, delivered two ways, must produce the same verdict. That property is what failed here, and it is cheap to pin.
Measured on ConductionNL/.github@923f57d locally and against CI runs using 5e73e640 and e3da4103; the split is present in all three.
Summary
Since
#378made whole-tree the default, gates 19, 25 and 26 are still silently diff-scoped on everypull_requestrun — i.e. on exactly the event that gates a merge. They report their whole-tree backlog only onpush, after the merge, where it can no longer block anything.The cause is not the scope flag. It is that those three helpers receive the base ref through the environment, and
HYDRA_GATE_BASE_REFis present in the environment onpull_requestand absent onpush.Isolated control — same tree, same base commit, only the delivery channel differs
Local run of
bin/hydra-gatesagainst docudesk, basef1931874in both arms,SCOPE-MODE: fullin both:HYDRA_GATE_BASE_REF=f1931874 bin/hydra-gates --app-dir Denv -u HYDRA_GATE_BASE_REF bin/hydra-gates --app-dir D --base f1931874NOT APPLICABLEFAIL — 396 scenario(s)NOT APPLICABLEPASS(it actually ran)NOT APPLICABLEFAIL — 6 new page component(s)COVERAGE: 56 of 65COVERAGE: 58 of 65Both arms print the same resolved base — only the source label changes (
($HYDRA_GATE_BASE_REF)vs(--base)). Nothing else about the invocation differs.The same split reproduces in CI as a push-vs-PR difference
push(2 changed files)Delta base: 51e128e… (github.event.before (push))pull_request(4 changed files)Delta base: origin/development ($HYDRA_GATE_BASE_REF)push(0 changed files)Delta base: 18e17b1f… (github.event.before (push))pull_request(1 changed file)Delta base: origin/development ($HYDRA_GATE_BASE_REF)On
pushthe wrapper resolves the base fromgithub.event.beforeinto a shell variable and forwards it as an argument; the env var is not set, so the helper sees no base and sweeps the tree. Onpull_requestthe caller exportsHYDRA_GATE_BASE_REF, and the helper picks it up regardless ofSCOPE_TO_DIFF.Why the existing guard does not catch it
run-hydra-gates.shgate-19 already knows about this class — its comment cites#242:and the code correctly gates the explicit pass:
The
elsebranch does not unset the variable. An inheritedHYDRA_GATE_BASE_REFwalks straight past theifinto the child process, and#242is re-opened by the environment rather than by the argument.HYDRA_GATE_BASE_REF=…and gotNOT APPLICABLEon a tree I already knew carried 396. That reads exactly like a clean repo.Impact
#378's stated purpose is that a tightened gate should force the inherited debt to be fixed. For the three coverage gates that does not happen where it would bite:And the two events produce numbers that look like progress. docudesk
developmentpush readsRESULT: 5 GATE(S) FAILED; a one-file PR against it readsRESULT: 2 GATE(S) FAILED. Nothing was fixed in between. Anyone quoting a PR gate count as a repo state — or comparing one to a push count — records a three-gate improvement that does not exist.Suggested fix
Unset the variable in the
elsebranch for each of the three helpers, so the environment cannot re-introduce the scoping the flag declined:#378— but it should land deliberately rather than as a surprise on someone's unrelated one-line change.Worth adding to the gate-acceptance fixtures as a two-arm case: same base, delivered two ways, must produce the same verdict. That property is what failed here, and it is cheap to pin.
Measured on
ConductionNL/.github@923f57dlocally and against CI runs using5e73e640ande3da4103; the split is present in all three.