fix(quality): a missing composer gate script must fail, not pass vacuously - #123
Merged
Merged
Conversation
…ously
`composer <script>` exits 1 both when the script runs and returns 1 and when
the script does not exist ("Command \"phpcs\" is not defined."). The phpcs leg
maps exit 1 to success to keep PHPCS warnings non-blocking, so any repo with no
`phpcs` composer script got a green "PHP Quality (phpcs)" tick forever — a
gate's absence and a gate's success were indistinguishable in the checks list.
Assert the script is declared in composer.json (via jq) before running it, on
every PHP quality leg. The other legs already went red on a missing script, but
with the opaque `Command "phpmd" is not defined.`; they now get an actionable
message naming the input that turns the gate off.
Deliberately not an `[ -f vendor/bin/<tool> ]` guard — a file-presence guard
that skips reintroduces the same silent-pass defect.
Closes #121
rubenvdlinde
added a commit
that referenced
this pull request
Aug 21, 2026
This workflow ends by opening a pull request that brings the integration
branch up to the version it just tagged. Merging that pull request is a push
to the integration branch, which triggers this workflow again, which bumps
the version again, which opens another pull request.
The bump commit carries `[skip ci]`, but the MERGE COMMIT that actually lands
on the branch does not — so the marker never gets a chance to work.
Measured on opencatalogi, 2026-08-20, over its last 100 development commits:
45 chore(release): version bumps
46 merges of release/* branches
9 actual work
91% of the branch's history was the workflow reacting to itself, one cycle
about every ten minutes, and the base version never advanced: every one of
those releases was 1.0.9-unstable.<timestamp>.
It hides well. Each run is a legitimate release of a real commit, so the only
symptom is that the branch never settles — which reads as "someone is
committing a lot" rather than as a defect. The damage shows up elsewhere: a
development branch that moves every ten minutes cancels its own in-flight CI
by concurrency, and opencatalogi's dev→beta pull request had its E2E and
Hydra Gates jobs cancelled repeatedly for exactly that reason. A cancelled
job yields no verdict, so the promotion could not be judged at all.
The guard skips the job when the triggering push IS the bump landing, in
either merge style: a merge commit names the `release/v*` head branch, and a
squash produces `chore(release): <version> (#123)`. Nothing is lost — those
pushes contain only appinfo/info.xml (and openapi.json where present), so
re-releasing them would build a byte-identical artefact under a new number.
`workflow_dispatch` is never skipped, so a human can still force a release by
hand, including of a commit this guard would refuse.
Verified with a truth table over REAL commit messages from opencatalogi's
history, asserting both directions rather than only the skip: the two bump
shapes and the squash shape skip, while `feat/rotterdam-demo-rig`, a plain
`test(federation):` commit and a `chore/nc-vue-2.8.2` dependency bump all
still release. A guard that only proved it can skip would be indisttinguishable
from one that skips everything.
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #121.
The hole
composer <script>exits 1 for two very different things:Command "phpcs" is not defined.— there is no such script.The
phpcsleg maps exit 1 to success (PHPCS warnings are non-blocking bypolicy), so any repo with no
phpcscomposer script got a permanently greenPHP Quality (phpcs)tick. A gate's absence and a gate's success wereindistinguishable in the checks list.
Before
The comment describes phpcs's exit codes;
RCholds composer's.After
A
require_scripthelper reads the declaration fromcomposer.json(viajq,already used 46× in this workflow) before the script is run, on every PHP
quality leg:
Deliberately not an
if [ -f vendor/bin/<tool> ]guard — a file-presenceguard that skips is the same silent-pass defect in a new costume. (Also, a
bare
phpmd/psalmin a composer script resolves fine withoutvendor/bin/,because Composer prepends
vendor/bintoPATH.)lint,phpmd,psalm,phpstanandphpmetricsalready went red on amissing script — but with the opaque
Command "phpmd" is not defined.They nowget an actionable message naming the input that turns the gate off. Their
red/green verdict is unchanged.
Proof — three live CI cells on
ConductionNL/petstoreThrowaway branch
feature/dotgithub-121-proof. One variable changes per cell.PHP Quality (phpcs)phpcsphpcsphpcs@main(unfixed)Cell C,
@main, verbatim — the vacuous pass reproduced live:Cell B, this branch, same repo state — now red with an actionable message:
Cell A, this branch, real script present — still green:
→ job conclusion
success,echo "success" > quality-results/phpcs.txt.C→B is the discriminator: identical repo state, only the reusable-workflow ref
differs, and the verdict flips. A→B holds the ref fixed and flips only the
script's existence.
Blast radius — exactly one repo, one leg
All 19 consumers of this workflow in the org were swept for the six gate scripts
in their default-branch
composer.json:PHP Quality (phpcs)flips green → redapp-versionsuses php-cs-fixer, not phpcs, and declares nophpcs,phpmd,phpstanorphpmetricsscript while leavingenable-phpcsat itstruedefault. Its
phpmd/phpstan/phpmetricslegs are already red forexactly this reason; only
phpcswas being swallowed. Turning that tick red isthe point of this change — but it is the one CI status in the fleet that moves.
Fix on that side is one line:
enable-phpcs: falseinapp-versions/.github/workflows/code-quality.yml(see ConductionNL/versioniq#118).Not touched
global-settings/is unmodified, so noVERSIONbump is required.