Skip to content

Commit 5b9f022

Browse files
authored
fix(ci): make the gates capable of both passing and failing (#9)
* fix(ci): make the gates capable of both passing and failing 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> * fix(psalm): the 13 errors CI was actually failing on, and a wrong diagnosis 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>
1 parent 6d766ee commit 5b9f022

7 files changed

Lines changed: 74 additions & 18 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,19 @@ jobs:
1717
coverage: none
1818
- name: Composer install
1919
run: composer install --prefer-dist --no-interaction --no-progress
20+
# Without `|| true`. Every one of these swallowed its exit code, so this
21+
# job reported success whatever happened — the only check in the repository
22+
# that could not fail, sitting next to a Tests workflow that could not
23+
# pass. Neither told anyone anything.
24+
#
25+
# Pest is scoped to the gate suites for the same reason tests.yml is: the
26+
# Module suite is tracked debt, reported by its own non-gating job.
2027
- name: Pint
21-
run: vendor/bin/pint --test || true
28+
run: vendor/bin/pint --test
2229
- name: PHPStan
23-
run: vendor/bin/phpstan analyse --no-progress || true
30+
run: vendor/bin/phpstan analyse --no-progress
31+
# No --no-interaction: that is a PHPUnit flag, and Pest rejects it with
32+
# `Unknown option` and exit 2. The `|| true` had been hiding a broken
33+
# invocation, not merely failing tests — this step has never run the suite.
2434
- name: Pest
25-
run: vendor/bin/pest --no-coverage --no-interaction || true
35+
run: vendor/bin/pest --testsuite=Feature,Unit --no-coverage

‎.github/workflows/tests.yml‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ jobs:
1111
runs-on: ubuntu-latest
1212

1313
strategy:
14-
fail-fast: true
14+
# One leg failing should not hide the others. With fail-fast the first
15+
# failure cancels the rest, so a PHP-version-specific break looks
16+
# identical to a break on every version.
17+
fail-fast: false
1518
matrix:
1619
php: [8.2, 8.3, 8.4]
1720
laravel: [12.*]
@@ -36,8 +39,14 @@ jobs:
3639
composer require "laravel/framework:${LARAVEL_VERSION}" --no-interaction --no-update
3740
composer update --prefer-dist --no-interaction --no-progress
3841
42+
# The gate suites only. A bare `phpunit` also runs the Module suite —
43+
# src/**/*Test.php — which carries known failures and is treated as
44+
# visible debt rather than a gate (see .gitlab-ci.yml's module-suite-debt
45+
# job, and the module-debt job below). Including it here meant this
46+
# workflow could never be green, so it stopped meaning anything: the same
47+
# rot as a check that always passes, arrived at from the other side.
3948
- name: Execute tests with coverage
40-
run: vendor/bin/phpunit --coverage-clover=coverage.xml
49+
run: vendor/bin/phpunit --testsuite=Feature,Unit --coverage-clover=coverage.xml
4150

4251
- name: Upload coverage to Codecov
4352
if: matrix.php == '8.3'
@@ -46,3 +55,40 @@ jobs:
4655
files: ./coverage.xml
4756
fail_ci_if_error: false
4857
verbose: true
58+
59+
# The Module suite — src/**/*Test.php — reported, never gating.
60+
#
61+
# It carries known failures, and the number is worth seeing rather than
62+
# hiding: 448 at the time of writing, and they are not 448 bugs. Roughly 114
63+
# are class-not-found for classes owned by sibling packages (Core\Tenant\Models,
64+
# Core\Agentic\Services) — tests that cannot pass in this repository at all;
65+
# about 30 are one real defect, StorageUrlResolver being handed a CdnUrlBuilder
66+
# where it wants a BunnyStorageService; the rest are ordinary assertion and
67+
# status-code failures.
68+
#
69+
# continue-on-error rather than `|| true`, and the difference matters: this
70+
# way the step's own result is visible in the run, so the count can be driven
71+
# down. A step that swallows its exit code teaches nobody anything.
72+
#
73+
# When it reaches zero, fold --testsuite=Module into the job above and delete
74+
# this one. Mirrors .gitlab-ci.yml's module-suite-debt job.
75+
module-debt:
76+
runs-on: ubuntu-latest
77+
name: Module suite (reported, not gating)
78+
continue-on-error: true
79+
steps:
80+
- name: Checkout code
81+
uses: actions/checkout@v4
82+
83+
- name: Setup PHP
84+
uses: shivammathur/setup-php@v2
85+
with:
86+
php-version: '8.4'
87+
extensions: dom, curl, libxml, mbstring, zip
88+
coverage: none
89+
90+
- name: Install dependencies
91+
run: composer install --prefer-dist --no-interaction --no-progress
92+
93+
- name: Execute the Module suite
94+
run: vendor/bin/phpunit --testsuite=Module

‎src/Core/Cdn/Models/StorageOffload.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public function scopeForDisk($query, string $disk)
150150
/**
151151
* Get human-readable file size.
152152
*/
153-
public function getFileSizeHumanAttribute(): string
153+
protected function getFileSizeHumanAttribute(): string
154154
{
155155
$bytes = $this->file_size ?? 0;
156156
$units = ['B', 'KB', 'MB', 'GB', 'TB'];

‎src/Core/Config/Models/ConfigValue.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class ConfigValue extends Model
6464
/**
6565
* Get the value attribute with automatic decryption for sensitive keys.
6666
*/
67-
public function getValueAttribute(mixed $value): mixed
67+
protected function getValueAttribute(mixed $value): mixed
6868
{
6969
if ($value === null) {
7070
return null;
@@ -91,7 +91,7 @@ public function getValueAttribute(mixed $value): mixed
9191
/**
9292
* Set the value attribute with automatic encryption for sensitive keys.
9393
*/
94-
public function setValueAttribute(mixed $value): void
94+
protected function setValueAttribute(mixed $value): void
9595
{
9696
// Check if the key is sensitive (need to load it if not already)
9797
$key = $this->relationLoaded('key')

‎src/Core/Media/Image/ImageOptimization.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public function scopeForWorkspace($query, ?Model $workspace)
100100
*
101101
* Example: "45% saved (120KB → 66KB)"
102102
*/
103-
public function getSavingsHumanAttribute(): string
103+
protected function getSavingsHumanAttribute(): string
104104
{
105105
// Format with appropriate unit
106106
$original = $this->formatBytes($this->original_size);
@@ -117,7 +117,7 @@ public function getSavingsHumanAttribute(): string
117117
/**
118118
* Get human-readable size saved.
119119
*/
120-
public function getSizeSavedHumanAttribute(): string
120+
protected function getSizeSavedHumanAttribute(): string
121121
{
122122
$saved = $this->original_size - $this->optimized_size;
123123

‎src/Core/Seo/Models/SeoScoreHistory.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public function seoMetadata(): BelongsTo
108108
/**
109109
* Get the score color for UI display.
110110
*/
111-
public function getScoreColorAttribute(): string
111+
protected function getScoreColorAttribute(): string
112112
{
113113
return match (true) {
114114
$this->score >= 80 => 'green',
@@ -120,7 +120,7 @@ public function getScoreColorAttribute(): string
120120
/**
121121
* Get the issue count.
122122
*/
123-
public function getIssueCountAttribute(): int
123+
protected function getIssueCountAttribute(): int
124124
{
125125
return count($this->issues ?? []);
126126
}

‎src/Core/Seo/SeoMetadata.php‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class SeoMetadata extends Model
9797
*
9898
* @return array<string, mixed>|null
9999
*/
100-
public function getSchemaMarkupAttribute(): ?array
100+
protected function getSchemaMarkupAttribute(): ?array
101101
{
102102
if ($this->schemaMarkupLoaded) {
103103
return $this->parsedSchemaMarkup;
@@ -132,7 +132,7 @@ public function getSchemaMarkupAttribute(): ?array
132132
*
133133
* @param array<string, mixed>|string|null $value
134134
*/
135-
public function setSchemaMarkupAttribute(array|string|null $value): void
135+
protected function setSchemaMarkupAttribute(array|string|null $value): void
136136
{
137137
// Reset the lazy loading cache
138138
$this->parsedSchemaMarkup = null;
@@ -183,7 +183,7 @@ public function seoable(): MorphTo
183183
*
184184
* Uses JSON_HEX_TAG to prevent XSS via </script> in content.
185185
*/
186-
public function getJsonLdAttribute(): string
186+
protected function getJsonLdAttribute(): string
187187
{
188188
if (empty($this->schema_markup)) {
189189
return '';
@@ -197,7 +197,7 @@ public function getJsonLdAttribute(): string
197197
/**
198198
* Generate all meta tags as HTML.
199199
*/
200-
public function getMetaTagsAttribute(): string
200+
protected function getMetaTagsAttribute(): string
201201
{
202202
$tags = [];
203203

@@ -241,7 +241,7 @@ public function getMetaTagsAttribute(): string
241241
/**
242242
* Get SEO score colour for UI display.
243243
*/
244-
public function getScoreColorAttribute(): string
244+
protected function getScoreColorAttribute(): string
245245
{
246246
if ($this->seo_score === null) {
247247
return 'zinc';
@@ -265,7 +265,7 @@ public function hasIssues(): bool
265265
/**
266266
* Get the count of issues.
267267
*/
268-
public function getIssueCountAttribute(): int
268+
protected function getIssueCountAttribute(): int
269269
{
270270
return count($this->seo_issues ?? []);
271271
}

0 commit comments

Comments
 (0)