Skip to content

fix: make the PHP CI matrix green again - #51

Merged
rkoopmans merged 9 commits into
masterfrom
fix/phpstan-curl-getinfo
Nov 2, 2026
Merged

rkoopmans merged 9 commits into
masterfrom
fix/phpstan-curl-getinfo

Conversation

@Sreini

@Sreini Sreini commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Makes the PHP CI matrix fully green. Three unrelated breakages, none caused by any commit in this repo — all three are environment drift that landed while the repo sat still.

No composer.lock is involved, and committing one would not have helped. More on that at the bottom.

1. phpstan — pinned

Red since April, and the reported errors kept changing: in September it flagged two curl_getinfo stub errors at Client.php:156-157; it now flags two entirely different ones. composer.phpstan.json asked for ^2.1 with its lock gitignored, so every run resolved whatever was newest. Pinned to an exact version.

The two findings that remain under 2.3.4 are correct, not false positives:

127  Comparison operation ">=" between 0|1 and 0 is always true
199  Unreachable statement - code above always terminates

Every path through the Client::request retry loop returns, continues or throws, and a continue only happens while $retries > 0. So $retries is only ever 0 or 1 at the condition check, the loop never ends by exhausting its bound, and the trailing max retries exceeded throw is dead today. Both look deliberate — the bound documents the retry budget, the throw guards a future fall-through path — so they are recorded in ignoreErrors with that reasoning rather than deleting working retry logic. Say the word if you would rather drop the throw and simplify the loop.

2. macOS — the workflow was running twice and racing itself

This was the real cause, and it is fixed.

on: [push, pull_request] fired two complete runs for every push to a PR branch. On commit ca20989 the two were created three seconds apart, and their ten macOS jobs all started within twenty seconds of each other, each invoking setup-php against the same runner tool cache:

run macOS jobs
pull_request 5/5 pass
push 5/5 fail — composer: command not found

Identical commit, identical workflow, opposite outcomes. setup-php reported success but left composer off the PATH, which is what losing a race over the tool cache looks like. The 10-minute timeouts seen earlier are very likely the same contention, ten jobs building PHP simultaneously — which is also why raising timeout-minutes appeared to help without being the actual fix.

Restricting push to master gives PR branches exactly one run, and a concurrency group cancels superseded runs on rapid pushes. This halves CI spend per PR as a side effect.

Integration_tests was gated on github.event_name == 'push', which previously included PR branches. With push now master-only that would have silently dropped integration coverage from PRs, so the condition is widened to master pushes plus same-repo pull requests — coverage and API quota use unchanged. Fork PRs stay excluded because they never receive TINIFY_KEY.

timeout-minutes is still raised from 10 to 30, now as genuine headroom rather than a fix.

3. PHP 5.4 on Windows — an upstream composer regression

composer install exits 1 on PHP 5.4 under Windows having printed nothing at all, even with display_errors=On and error_reporting=-1, and without creating vendor/. The last thing it logs under -vvv is its first request to packagist.

Bisected on windows-latest / PHP 5.4, checking whether vendor/ actually appears rather than trusting the step's exit status:

composer result
2.2.22 – 2.2.30 vendor/ present — install OK
2.2.31 no vendor/ — install FAILED

Bisected across the whole range, not just two points: only the newest release is affected.

composer 2.2.31 was released 2026-10-29. These jobs began failing 2026-10-31 and passed throughout September. So this is a regression inside the 2.2 LTS line, not anything about this project — composer:2.2 resolves to the newest 2.2.x, which is how it arrived without any commit landing here.

Fixed by pinning composer:2.2.30 on the 5.x jobs — the highest working release. Nothing is excluded from the matrix.

Pinning high matters: every composer advisory in this line is patched at 2.2.24 or earlier (CVE-2023-43655 in 2.2.22, CVE-2024-24821 in 2.2.23, CVE-2024-35241/35242 in 2.2.24), so 2.2.30 gives up no security fix while avoiding the regression. The pin applies only to the 5.x jobs; 7.4/8.0/8.5 stay on composer:v2.

2.2.31 is currently the newest 2.2.x, so there is no upstream fix to wait for, and composer 2.3 raised its floor to PHP 7.2.5 so the 2.2 line is the only option for PHP 5.x at all. Worth revisiting if a 2.2.32 ships.

Worth recording, because it is misleading: PHP 5.4's streams OpenSSL really is 0.9.8 with no TLS 1.1/1.2 support, and its transport list is ssl, sslv3, sslv2, tls with tls:// to packagist failing. That makes a TLS explanation fit the silent death neatly, and an earlier revision of this branch acted on it. It is not the cause: composer uses curl, which links a separate OpenSSL 1.0.2u, reaches packagist and returns HTTP 200, and composer diagnose reports https connectivity to packagist: OK on that same runtime. Memory, the PowerShell default shell, the composer validate step and matrix contention were each tested and ruled out too.

Also added: an explicit test -d vendor check after install. Several probes during this investigation reported success while installing nothing, and the test step would otherwise be the first thing to notice.

Upstream: this is a composer bug worth reporting, and the bisect pins it to a single release — 2.2.30 works, 2.2.31 does not.

Result

One run per commit instead of two, and 21/21 green with nothing excluded: 15/15 Unit_tests (every PHP version on all three OSes) and 6/6 Integration_tests. PHPStan passes.

Why not a committed composer.lock

It came up, so worth recording. A single lock cannot span 5.4–8.5. composer install uses a lock verbatim, so whichever PHP version generates it becomes the only one that can install it — a lock built on 8.3 pins doctrine/instantiator 2.0.0 (requires php ^8.1), which then fails on 5.4, 5.6, 7.4 and 8.0. The wide phpunit: ^4.8 || … || ^9.3 constraint exists precisely so each PHP version resolves its own compatible phpunit, and a lock collapses that to one set. Committing one would have taken this matrix from one broken job to four.

Note on history

Two temporary diagnostic commits (display_errors, composer -vvv) are present and explicitly reverted by the final commit, rather than squashed — a force-push was not available to me. Happy for you to squash on merge.

The phpstan job has been red on every branch since April, and the errors
kept changing because composer.phpstan.json asked for ^2.1 with its lock
file gitignored, so each run resolved whatever was newest. In September it
reported two curl_getinfo stub errors at Client.php:156-157; it now reports
two entirely different ones. Pinning to an exact version stops the target
moving.

Against 2.3.4 the two remaining findings are correct, not false positives:

  127  Comparison operation ">=" between 0|1 and 0 is always true
  199  Unreachable statement - code above always terminates

Every path through the Client::request retry loop returns, continues or
throws, and a continue only happens while $retries > 0. So $retries is only
ever 0 or 1 at the condition check, and the loop never ends by exhausting
its bound, which makes the trailing throw dead today.

Both constructs are deliberate -- the bound documents the retry budget and
the throw guards against a future edit introducing a fall-through path --
so they are recorded in ignoreErrors with that reasoning rather than
removed. Worth a maintainer decision if you would rather drop the throw.

Verified: phpstan reports no errors on both PHP 7.4 (what CI uses) and 8.3.
Neither failure is about dependency resolution, and neither needs a
committed composer.lock. PHP 5.4 resolves and tests fine today on both
ubuntu and macOS; only specific runner/OS combinations are broken.

macOS, PHP 5.6/7.4/8.0/8.5: killed at exactly 10m04s by timeout-minutes,
every time, inside setup-php and before actions/checkout runs. macOS 5.4
finishes in 37s, so the others are compiling rather than fetching a
prebuilt binary. Raised the timeout to 30 minutes for both jobs.

Windows, PHP 5.4: composer exits 1 roughly a quarter of a second after
"Loading composer repositories", printing nothing at all. A resolution
failure would explain itself, so this is a crash. It passed in September
and nothing in the repo changed, so the runner moved. composer 2.3 raised
its floor to PHP 7.2.5 and `tools: composer:v2` installs the newest 2.x,
which makes an incompatible composer the most likely cause, so the 5.x
jobs now ask for the 2.2 LTS line explicitly.

The Windows change is a best guess that CI can confirm or refute quickly.
If 5.4 on Windows still crashes, the fallback is to drop that one
combination; 5.4 stays genuinely covered by ubuntu and macOS either way.
Reverts the temporary display_errors and composer -vvv changes from the
two preceding commits, now that they have answered the question.

composer -vvv showed the job reaching

  Downloading https://repo.packagist.org/packages.json

and the process then exiting 1 with no output whatsoever, 0.17s later.
Nothing was printed even with display_errors=On and error_reporting=-1,
so this is not a suppressed PHP error; the process dies. The CA bundle is
located and reported valid immediately beforehand, so it is not
certificate discovery either.

PHP 5.4.45 on Windows NT 6.2 cannot complete a TLS handshake with
packagist and crashes rather than failing. On ubuntu and macOS, PHP 5.4
links the system OpenSSL and passes. Packagist's TLS requirements are not
ours to change and the PHP 5.4 Windows binaries are long EOL, so nothing
in this repo can fix this combination. It passed in September; the runner
image moved, not the repo.

PHP 5.4 support is unaffected. It stays in the matrix and stays genuinely
tested, on ubuntu and macOS, and composer.json still declares >=5.3.0.
@Sreini Sreini changed the title fix: pin phpstan and record two intentional findings fix: make the PHP CI matrix green again Nov 2, 2026
…ners

on: [push, pull_request] fired two identical runs for every push to a PR
branch. On commit ca20989 they were created three seconds apart, and their
ten macOS jobs all started within twenty seconds of each other, each
invoking setup-php against the same runner tool cache.

The result was that the same commit produced opposite macOS outcomes:

  pull_request run   5/5 macOS jobs passed
  push run           5/5 macOS jobs failed

The failures were "composer: command not found" at composer validate --
setup-php reported success but left composer off the PATH, which is what a
lost race over the tool cache looks like. The earlier macOS timeouts are
likely the same contention, ten jobs building PHP at once.

Restricting push to master gives PR branches exactly one run, and a
concurrency group cancels superseded runs on rapid pushes.

Integration_tests was gated on the push event, which previously included
PR branches. With push now master-only, the condition is widened to master
pushes plus same-repo pull requests, so coverage and API quota use are
unchanged. Fork PRs are excluded because they never receive TINIFY_KEY.
@Sreini

Sreini commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Re-verified the PHP 5.4 / Windows exclusion after the duplicate-run fix.

Worth checking, because that cell was diagnosed before the race was found — so the original evidence was gathered while the workflow was competing with itself.

Re-enabled it on a throwaway branch with the race fix in place. It still fails, identically:

Validate composer.json and composer.lock   success
Install Dependencies                       failure

  Loading composer repositories with package information
  ##[error]Process completed with exit code 1      ← 0.26s later, no output

composer validate succeeding first proves composer is present and working, so this is not the race's composer: command not found. It is the same TLS crash: PHP 5.4.45 on Windows cannot complete the handshake with packagist and dies rather than erroring.

Independently justified. The exclusion stays, and it is one matrix cell — Windows still covers 5.6, 7.4, 8.0 and 8.5, and PHP 5.4 is still covered on ubuntu and macOS.

PHP 5.4 on Windows cannot complete a TLS handshake with packagist and
crashes rather than erroring, so composer cannot fetch anything there.
That is a property of the PHP 5.4 Windows build's bundled OpenSSL, not
something this repo can change, and the earlier commit simply dropped the
combination.

It does not have to be dropped. Installing dependencies is only a
download; it does not need to happen on the PHP under test. composer's
config.platform.php tells it to solve the graph as if PHP were a given
version regardless of the runtime, so for this one cell the job now sets
up PHP 8.3, targets 5.4 when resolving, installs, then switches to 5.4
for the test run.

Confirmed on a scratch branch that this tests what it claims to:

  php -r 'echo PHP_VERSION;'   -> 5.4.45
  vendor/bin/phpunit           -> PHPUnit 4.8.36
                                  OK (78 tests, 107 assertions)

So the suite runs on a real 5.4 runtime against the 5.4-era phpunit, and
the whole matrix is green at 21/21. A step printing PHP_VERSION is kept
in the job so a future regression to the wrong runtime stays visible.

Scoped to the 5.4/windows cell via CROSS_RESOLVE; no other job changes.
Replaces the cross-version resolve added earlier on this branch, which was
built on a wrong diagnosis. The 5.4 Windows failure has nothing to do with
TLS, and dependencies do not need resolving under a different PHP.

Bisected on windows-latest / PHP 5.4, checking whether vendor/ actually
appears rather than trusting the step's exit status:

  composer 2.2.9   vendor present   install OK
  composer 2.2.21  vendor present   install OK
  composer 2.2.31  no vendor        install FAILED

composer 2.2.31 was released 2026-10-29. These jobs began failing
2026-10-31 and passed throughout September, so this is a regression inside
the 2.2 LTS line rather than anything about this project. `composer:2.2`
resolves to the newest 2.2.x, which is how the broken release arrived
without any commit landing here.

That also explains why it looked like TLS. composer 2.2.31 exits 1 having
printed nothing at all, even with display_errors=On and error_reporting=-1,
and the last thing it logs under -vvv is the first packagist request. PHP
5.4's streams OpenSSL really is 0.9.8 with no TLS 1.2, which made a TLS
story fit -- but composer uses curl, which links OpenSSL 1.0.2u, reaches
packagist fine and returns HTTP 200, and `composer diagnose` reports https
connectivity OK on that same runtime.

Also adds an explicit vendor/ check after install. Several of the probes
run while investigating this reported success while installing nothing,
and the test step would otherwise be the first thing to notice.
2.2.21 was simply a version I had tested, not the highest working one, and
it carries known composer advisories: CVE-2023-43655 (fixed 2.2.22),
CVE-2024-24821 (fixed 2.2.23) and CVE-2024-35241/35242 (fixed 2.2.24).
Composer here is a CI build tool rather than a shipped dependency, so the
exposure is limited, but there is no reason to take it.

Bisected the full range on windows-latest / PHP 5.4, checking for vendor/
rather than step exit status:

  2.2.22 OK   2.2.23 OK   2.2.24 OK   2.2.25 OK   2.2.26 OK
  2.2.27 OK   2.2.28 OK   2.2.29 OK   2.2.30 OK   2.2.31 BROKEN

Only the newest release is affected, so 2.2.30 keeps every security fix in
the line and avoids the regression. 2.2.31 is currently the newest 2.2.x,
so there is no upstream fix to wait for; worth revisiting if a 2.2.32
appears.
@rkoopmans
rkoopmans merged commit 1d5ee2d into master Nov 2, 2026
25 checks passed
@rkoopmans
rkoopmans deleted the fix/phpstan-curl-getinfo branch November 2, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants