Skip to content

fix(ci): make the gates capable of both passing and failing - #9

Merged
Snider merged 2 commits into
mainfrom
fix/honest-gates
Aug 8, 2026
Merged

fix(ci): make the gates capable of both passing and failing#9
Snider merged 2 commits into
mainfrom
fix/honest-gates

Conversation

@Snider

@Snider Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

main was red, with two opposite problems sitting next to each other: a check that could not fail, beside a check that could not pass.

Psalm — the suppression whose only effect was to be unused

psalm.xml suppressed NoEnvOutsideConfig for src/. Psalm reported UnusedIssueHandlerSuppression, because the issue is never raised: src/'s 171 env() calls live in files literally named config.php, which the Laravel plugin already treats as config.

So the suppression protected nothing, and its own unusedness was the only error Psalm found.

before   exit 2   1 error    1486 info-level issues
after    exit 0   No errors  1486 info-level issues

Tests — a workflow that could never be green

tests.yml ran a bare phpunit, which includes the Module suite (src/**/*Test.php) — 448 known failures, treated everywhere else as visible debt rather than a gate.

exit
phpunit (bare, what it ran) 1
phpunit --testsuite=Feature,Unit 0 — 269 tests

It now runs the gate suites, and the Module suite gets its own job with continue-on-error: true so the number stays visible and can be driven down. This mirrors .gitlab-ci.yml, which had already made exactly this split with its reasoning written out — the two CIs disagreed, and GitLab was right.

fail-fast is now off on the matrix: with it on, the first failing leg cancelled the others, so a break specific to one PHP version looked identical to a break on all three. That is why the reported red leg kept moving between 8.3 and 8.4.

And the other direction

ci.yml ran pint, phpstan and pest each with || true. It was the one check in the repository that could not fail. Removed, all three.

Doing that surfaced what the || true had been hiding:

$ vendor/bin/pest --no-coverage --no-interaction
INFO  Unknown option "--no-interaction".      # exit 2

--no-interaction is a PHPUnit flag; Pest rejects it. That step has never run the suite — not "ran and was ignored", never ran. A check that swallows its exit code cannot tell you it is not the check you think it is.

Verified locally, every gate

pint --test                            exit 0
phpstan analyse                        exit 0
psalm --show-info=false                exit 0   (was 2)
pest --testsuite=Feature,Unit          exit 0   (was 2, on the bad flag)
phpunit --testsuite=Feature,Unit       exit 0   (was 1, via the debt suite)
phpunit --testsuite=Module             exit 1   — expected, now non-gating

The 448 is not 448 bugs

The debt job says so in its comment, so the next person starts from the breakdown rather than the total:

  • ~114 class-not-found for classes owned by sibling packages (Core\Tenant\Models, Core\Agentic\Services) — cannot pass in this repository at all
  • ~30 one real defect: StorageUrlResolver handed a CdnUrlBuilder where it wants a BunnyStorageService
  • the rest ordinary assertion and status-code failures

Left alone deliberately

ci.yml now overlaps static-analysis.yml on pint/phpstan and tests.yml on the suite. Duplicated gates are harmless; lying ones are not. Consolidating them is a tidy-up, not this change.

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

main was red, and had two opposite problems sitting next to each other.

PSALM. psalm.xml suppressed NoEnvOutsideConfig for src/, and Psalm reported
UnusedIssueHandlerSuppression because the issue is never raised — src/'s env()
calls live in files literally named config.php, which the Laravel plugin already
treats as config. So the suppression protected nothing and its own unusedness was
the only error Psalm found. Removing it takes Psalm from exit 2 to "No errors
found!" with the 1486 info-level issues unchanged. That was the whole Psalm half
of the red.

TESTS. tests.yml ran a bare `phpunit`, which includes the Module suite —
src/**/*Test.php, 448 known failures, treated everywhere else as visible debt
rather than a gate. So the workflow could never pass. It now runs
--testsuite=Feature,Unit, which is exit 0 with 269 tests, and the Module suite
gets its own job with continue-on-error so the number stays on screen and can be
driven down. That mirrors .gitlab-ci.yml, which had already made exactly this
split with the reasoning written out.

fail-fast is off on the matrix. With it on, the first failing leg cancelled the
others, so a break specific to one PHP version was indistinguishable from a break
on all three.

AND THE OTHER DIRECTION. ci.yml ran pint, phpstan and pest each with `|| true`.
It was the one check in the repository that could not fail, next to a Tests
workflow that could not pass. Removed, all three.

Doing that surfaced something the `|| true` had been hiding: the Pest step passed
--no-interaction, which is a PHPUnit flag. Pest answers `Unknown option
"--no-interaction"` and exits 2. That step has never run the suite once — not
"ran and was ignored", never ran. A check that swallows its exit code cannot tell
you it is not the check you think it is.

Verified locally, every gate:

  pint --test                            exit 0
  phpstan analyse                        exit 0
  psalm --show-info=false                exit 0   (was 2)
  pest --testsuite=Feature,Unit          exit 0   (was 2, on the bad flag)
  phpunit --testsuite=Feature,Unit       exit 0   (was 1, via the debt suite)
  phpunit --testsuite=Module             exit 1   — expected, now non-gating

The 448 is not 448 bugs, and the debt job says so: ~114 are class-not-found for
sibling packages' classes and cannot pass here at all, ~30 are one real defect
(StorageUrlResolver handed a CdnUrlBuilder where it wants a BunnyStorageService),
the rest ordinary assertion failures. Named there so the next person starts from
the breakdown rather than the total.

Left alone deliberately: ci.yml now overlaps static-analysis.yml on pint and
phpstan, and tests.yml on the suite. Duplicated gates are harmless; lying ones
are not. Consolidating them is a tidy-up, not this change.

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Snider, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: adb5d9ab-b98d-4a8e-bf57-c4616b6c54d5

📥 Commits

Reviewing files that changed from the base of the PR and between 6d766ee and 5deb47c.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • .github/workflows/tests.yml
  • src/Core/Cdn/Models/StorageOffload.php
  • src/Core/Config/Models/ConfigValue.php
  • src/Core/Media/Image/ImageOptimization.php
  • src/Core/Seo/Models/SeoScoreHistory.php
  • src/Core/Seo/SeoMetadata.php

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…gnosis undone

I diagnosed Psalm from local output and got it wrong twice. Correcting both here
rather than quietly.

WHAT I DID WRONG. Locally Psalm reported one error — UnusedIssueHandlerSuppression
for the NoEnvOutsideConfig suppression in psalm.xml — so I removed the suppression
and saw exit 0. In CI that removal produced 183 NoEnvOutsideConfig errors in
src/Core/config.php. The suppression was doing real work; it only looks unused
locally, because the Laravel plugin resolves the config directory differently
there. Reverted.

WHAT CI WAS ACTUALLY FAILING ON, which I should have read first: 13
PublicModelAccessor errors, none of which appear locally at all.

  6  src/Core/Seo/SeoMetadata.php
  2  src/Core/Seo/Models/SeoScoreHistory.php
  2  src/Core/Media/Image/ImageOptimization.php
  2  src/Core/Config/Models/ConfigValue.php
  1  src/Core/Cdn/Models/StorageOffload.php

Eloquent accessors and mutators declared public. Laravel reaches them through
getAttribute()/__get, so protected is what they should be, and public invites
somebody to call getValueAttribute() directly and depend on it.

Made protected, all 13. Checked first that nothing calls any of them directly —
not in this package, and not in host.uk.com, which is the only consumer — so the
visibility change costs nobody anything. All five classes extend Model, so the
framework path is the only path.

The lesson is the same one I flagged in another repo this morning and then walked
into: local and CI disagreed, and I trusted the copy in front of me instead of
opening the job log. Local Psalm reports 1 error / 1486 issues, CI reports 13 / 1155.
On a tool with a Laravel plugin that bootstraps differently per environment, the
CI log is the only source that speaks for CI.

  vendor/bin/pint --test                       pass
  vendor/bin/phpstan analyse                   no errors
  ./vendor/bin/pest --testsuite=Feature,Unit   268 passed, 0 failed — unchanged
  ./vendor/bin/pest --testsuite=Module         448 failed / 308 passed — unchanged

Psalm itself cannot be verified from here, for the reason above. CI is the check.

Co-Authored-By: Virgil <virgil@lethean.io>
@Snider

Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

CI verdict, and a correction

Every gate is green. The one red is Module suite (reported, not gating), which is continue-on-error: true and failing on purpose — that is the visible-debt job doing its job.

SUCCESS  PHP 8.2 / 8.3 / 8.4 - Laravel 12.*     <- all three, was red
SUCCESS  Psalm                                  <- was red
SUCCESS  test                                   <- and now honest, no || true
SUCCESS  PHPStan, Laravel Pint, PHP CodeSniffer, Security Audit, core-lint
FAILURE  Module suite (reported, not gating)     <- by design

I had the Psalm diagnosis wrong, twice

The description above says the Psalm half was an unused suppression in psalm.xml. That came from local output and it was wrong on both counts.

Removing that suppression produced 183 NoEnvOutsideConfig errors in CI. It was doing real work; it only looks unused locally, because the Laravel plugin resolves the config directory differently there. Reverted.

What CI was actually failing on — which I should have read before touching anything — was 13 PublicModelAccessor errors that do not appear locally at all:

file count
6
2
2
2
1

Eloquent accessors declared public. Laravel reaches them through getAttribute(), so protected is correct — and public invites someone to call getValueAttribute() directly and depend on it. Made protected, all 13, after checking nothing calls any of them directly in this package or in host.uk.com, the only consumer.

Local Psalm reports 1 error / 1486 issues; CI reports 13 / 1155. On a tool whose Laravel plugin bootstraps differently per environment, the CI log is the only thing that speaks for CI — which is the same lesson I flagged in another repo this morning and then walked straight into.

@Snider

Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Snider

Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my own comment above: the file column of that table came out empty. I wrote the comment through a double-quoted shell string and zsh executed the backticked paths as commands instead of passing them through — the same mistake I made on an earlier PR today. The table should read:

file PublicModelAccessor errors
src/Core/Seo/SeoMetadata.php 6
src/Core/Seo/Models/SeoScoreHistory.php 2
src/Core/Media/Image/ImageOptimization.php 2
src/Core/Config/Models/ConfigValue.php 2
src/Core/Cdn/Models/StorageOffload.php 1

13 total, all now protected.

@Snider
Snider merged commit 5b9f022 into main Aug 8, 2026
13 of 14 checks passed
@Snider
Snider deleted the fix/honest-gates branch August 8, 2026 11:57
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.

1 participant