Summary
On a pull request whose merge base predates the app's PHPUnit setup, the
coverage ratchet fails with no action available to the author. This is the
unclosable-gate shape. It bites development → main / development → beta
release-promotion PRs, where the merge base can be months old.
Confirmed on zaakafhandelapp PR #294 (development → main), run
31478794540,
job PHPUnit (PHP 8.3, NC stable32) (93739014068).
What the log shows
The tests are fine — Run PHPUnit tests is a green check:
OK (242 tests, 772 assertions)
The failing step is Measure merge-base coverage:
head=4fa6d02… base=b233af99… merge-base=b233af99…
HEAD is now at b233af9 chore(ci): add missing canonical workflows (#244)
No composer.lock file present. Updating dependencies to latest instead.
…
/home/runner/work/_temp/….sh: line 9: ./vendor/bin/phpunit: No such file or directory
##[error]Could not measure coverage at merge base b233af99… — refusing to report a ratchet that did not run.
Verified against the repo rather than inferred:
git cat-file -e b233af99…:composer.lock → fatal: Not a valid object name
git show b233af99…:composer.json has no phpunit/phpunit in require-dev
Today's development has both. So at the merge base the test tooling simply did
not exist yet, composer install resolves without phpunit, no base-clover.xml
is produced, and the test -s guard fires.
Why the single-cell shape is a red herring
quality.yml:1829 restricts the step to one matrix cell:
if: ${{ inputs.enable-coverage-guard && github.event_name == 'pull_request'
&& matrix.php-version == inputs.php-version
&& matrix.nextcloud-ref == fromJSON(inputs.nextcloud-test-refs)[0] }}
so 8.3/stable32 is the only cell that runs it. The other three "passes" never
executed the step. This shape is being misread across the fleet as an
environment-dependent test failure (class_exists() autoload) or a wiped
vendor/. Control pair that rules both out: the same commit 8a790664 passed
this exact cell on the push run 31478791392 and failed on the PR run — the only
difference is github.event_name.
The guard itself is correct — please do not soften it
The step's own comment explains why a missing/zero clover must be fatal: a base
measured as 0% sets the floor to zero and passes every possible drop. So all of
these are wrong fixes:
|| true on the clover check, or deleting the test -s guard
- treating an unmeasurable base as 0%
- skipping the ratchet whenever it is inconvenient
Suggested direction
The defect is that an unmeasurable base is reported as a failed base. A
merge base predating the suite has undefined coverage, not zero.
The trigger is cleanly detectable and distinguishable from "tests ran and
produced nothing": ./vendor/bin/phpunit absent after composer install at the
merge base. In that case the defensible fallback is the absolute check this
same workflow already runs on every push —
php scripts/coverage-guard.php coverage/clover.xml
against the committed .coverage-baseline. That is a real check rather than a
skip, so the ratchet is never silently demoted to nothing, and the PR author has
something they can actually satisfy.
Also affected
openconnector is reported to hit the same failure mode. Worth sweeping every repo
that sets enable-coverage-guard and has a long-lived main/beta divergence.
Not fixed in this issue, deliberately
I have authority to change .github, but this is a behaviour change to a workflow
every app inherits, and #347 is currently being held back specifically to avoid
reddening ten repos mid-flight. Filing the diagnosis rather than shipping the
change seemed the right trade; happy to implement it once the fleet is quiescent.
Summary
On a pull request whose merge base predates the app's PHPUnit setup, the
coverage ratchet fails with no action available to the author. This is the
unclosable-gate shape. It bites
development → main/development → betarelease-promotion PRs, where the merge base can be months old.
Confirmed on zaakafhandelapp PR #294 (
development → main), run31478794540,
job
PHPUnit (PHP 8.3, NC stable32)(93739014068).What the log shows
The tests are fine —
Run PHPUnit testsis a green check:The failing step is
Measure merge-base coverage:Verified against the repo rather than inferred:
git cat-file -e b233af99…:composer.lock→fatal: Not a valid object namegit show b233af99…:composer.jsonhas nophpunit/phpunitinrequire-devToday's
developmenthas both. So at the merge base the test tooling simply didnot exist yet,
composer installresolves without phpunit, nobase-clover.xmlis produced, and the
test -sguard fires.Why the single-cell shape is a red herring
quality.yml:1829restricts the step to one matrix cell:so 8.3/stable32 is the only cell that runs it. The other three "passes" never
executed the step. This shape is being misread across the fleet as an
environment-dependent test failure (
class_exists()autoload) or a wipedvendor/. Control pair that rules both out: the same commit8a790664passedthis exact cell on the push run 31478791392 and failed on the PR run — the only
difference is
github.event_name.The guard itself is correct — please do not soften it
The step's own comment explains why a missing/zero clover must be fatal: a base
measured as 0% sets the floor to zero and passes every possible drop. So all of
these are wrong fixes:
|| trueon the clover check, or deleting thetest -sguardSuggested direction
The defect is that an unmeasurable base is reported as a failed base. A
merge base predating the suite has undefined coverage, not zero.
The trigger is cleanly detectable and distinguishable from "tests ran and
produced nothing":
./vendor/bin/phpunitabsent aftercomposer installat themerge base. In that case the defensible fallback is the absolute check this
same workflow already runs on every push —
against the committed
.coverage-baseline. That is a real check rather than askip, so the ratchet is never silently demoted to nothing, and the PR author has
something they can actually satisfy.
Also affected
openconnector is reported to hit the same failure mode. Worth sweeping every repo
that sets
enable-coverage-guardand has a long-livedmain/betadivergence.Not fixed in this issue, deliberately
I have authority to change
.github, but this is a behaviour change to a workflowevery app inherits, and #347 is currently being held back specifically to avoid
reddening ten repos mid-flight. Filing the diagnosis rather than shipping the
change seemed the right trade; happy to implement it once the fleet is quiescent.