From d4b5a34b79dc705aa3e1912e1970d47b3ab4ca1d Mon Sep 17 00:00:00 2001 From: Jordan Lavar Partridge Date: Wed, 9 Sep 2026 23:13:42 -0700 Subject: [PATCH 1/3] chore: retire GitHub Actions quality for quality-runners CI workflow is a pointer only. Pint, Pest, PHPStan, and Rector run locally via ./bin/ci and on PRs via synapse-sentinel/quality-runners. --- .github/workflows/ci.yml | 31 +++-- README.md | 11 ++ app/Commands/DnsCreateCommand.php | 7 +- app/Commands/DnsListCommand.php | 10 +- app/Commands/TunnelDeleteCommand.php | 2 +- app/Commands/TunnelListCommand.php | 4 +- app/Commands/ZonesCommand.php | 5 +- .../Cloudflare/CloudflareConnector.php | 2 +- bin/ci | 17 +++ bootstrap/providers.php | 4 +- composer.json | 4 +- composer.lock | 123 +++++++++++++++++- config/app.php | 4 +- config/commands.php | 27 ++-- phpstan.neon | 5 + rector.php | 12 ++ tests/Feature/QualityRunnersCiTest.php | 34 +++++ tests/Pest.php | 8 +- tests/Unit/TunnelResourceTest.php | 6 +- 19 files changed, 276 insertions(+), 40 deletions(-) create mode 100755 bin/ci create mode 100644 phpstan.neon create mode 100644 rector.php create mode 100644 tests/Feature/QualityRunnersCiTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a28235a..242feea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,19 +1,28 @@ name: CI on: - push: - branches: [main, cursor/**] pull_request: + branches: [main] + push: + branches: [main] + +# Quality tools are not run on GitHub-hosted runners. +# synapse-sentinel/quality-runners clones the PR, runs Pint / Pest / PHPStan / Rector, +# and posts commit statuses (quality-runners/{tool}, quality-runners/gate). +# Merge still requires those green. Do not add a second runner here. +# https://github.com/synapse-sentinel/quality-runners + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - tests: + pointer: + name: quality-runners is the house gate runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: mbstring, xml - coverage: none - - run: composer install --no-interaction --prefer-dist - - run: vendor/bin/pest + - name: Point at the house runner + run: | + echo "conduit-ui/cloudflare quality is synapse-sentinel/quality-runners, not this workflow." + echo "https://github.com/synapse-sentinel/quality-runners" + echo "This job does not run Pint, Pest, PHPStan, or Rector." diff --git a/README.md b/README.md index a8df02e..87cc42a 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,17 @@ app/ │ └── Requests/ # Individual API requests ``` +## Development + +PR quality is [quality-runners](https://github.com/synapse-sentinel/quality-runners) (Pint, Pest, PHPStan, Rector). GitHub Actions does not run those tools. + +Locally: + +```bash +composer install +./bin/ci +``` + ## License MIT diff --git a/app/Commands/DnsCreateCommand.php b/app/Commands/DnsCreateCommand.php index 74e20f7..58d657c 100644 --- a/app/Commands/DnsCreateCommand.php +++ b/app/Commands/DnsCreateCommand.php @@ -30,6 +30,7 @@ public function handle(): int $zoneId = $this->resolveZoneId($connector, $zone); if (! $zoneId) { $this->error("Zone not found: {$zone}"); + return self::FAILURE; } $zone = $zoneId; @@ -44,7 +45,8 @@ public function handle(): int ); if (! $response->successful()) { - $this->error('Failed to create DNS record: ' . $response->body()); + $this->error('Failed to create DNS record: '.$response->body()); + return self::FAILURE; } @@ -52,6 +54,7 @@ public function handle(): int if ($this->option('json')) { $this->line(json_encode($record, JSON_PRETTY_PRINT)); + return self::SUCCESS; } @@ -72,8 +75,10 @@ protected function resolveZoneId(CloudflareConnector $connector, string $name): $response = $connector->zones()->list($name); if ($response->successful()) { $zones = $response->json('result', []); + return $zones[0]['id'] ?? null; } + return null; } diff --git a/app/Commands/DnsListCommand.php b/app/Commands/DnsListCommand.php index aa03855..af2916e 100644 --- a/app/Commands/DnsListCommand.php +++ b/app/Commands/DnsListCommand.php @@ -27,6 +27,7 @@ public function handle(): int $zoneId = $this->resolveZoneId($connector, $zone); if (! $zoneId) { $this->error("Zone not found: {$zone}"); + return self::FAILURE; } $zone = $zoneId; @@ -38,7 +39,8 @@ public function handle(): int ); if (! $response->successful()) { - $this->error('Failed to list DNS records: ' . $response->body()); + $this->error('Failed to list DNS records: '.$response->body()); + return self::FAILURE; } @@ -46,11 +48,13 @@ public function handle(): int if ($this->option('json')) { $this->line(json_encode($records, JSON_PRETTY_PRINT)); + return self::SUCCESS; } if (empty($records)) { $this->info('No DNS records found.'); + return self::SUCCESS; } @@ -73,15 +77,17 @@ protected function resolveZoneId(CloudflareConnector $connector, string $name): $response = $connector->zones()->list($name); if ($response->successful()) { $zones = $response->json('result', []); + return $zones[0]['id'] ?? null; } + return null; } protected function truncate(string $value, int $length): string { return strlen($value) > $length - ? substr($value, 0, $length - 3) . '...' + ? substr($value, 0, $length - 3).'...' : $value; } diff --git a/app/Commands/TunnelDeleteCommand.php b/app/Commands/TunnelDeleteCommand.php index 30e3ccf..eb56ca2 100644 --- a/app/Commands/TunnelDeleteCommand.php +++ b/app/Commands/TunnelDeleteCommand.php @@ -33,7 +33,7 @@ public function handle(): int $response = $connector->tunnels()->delete($tunnelId); if (! $response->successful()) { - $this->error('Failed to delete tunnel: ' . $response->body()); + $this->error('Failed to delete tunnel: '.$response->body()); return self::FAILURE; } diff --git a/app/Commands/TunnelListCommand.php b/app/Commands/TunnelListCommand.php index 726cb3e..dfe074d 100644 --- a/app/Commands/TunnelListCommand.php +++ b/app/Commands/TunnelListCommand.php @@ -22,7 +22,7 @@ public function handle(): int $response = $connector->tunnels()->list(); if (! $response->successful()) { - $this->error('Failed to list tunnels: ' . $response->body()); + $this->error('Failed to list tunnels: '.$response->body()); return self::FAILURE; } @@ -44,7 +44,7 @@ public function handle(): int $this->table( ['ID', 'Name', 'Status', 'Created', 'Connections'], collect($tunnels)->map(fn ($t) => [ - substr($t['id'], 0, 8) . '...', + substr($t['id'], 0, 8).'...', $t['name'], $t['status'] ?? 'unknown', substr($t['created_at'] ?? '', 0, 10), diff --git a/app/Commands/ZonesCommand.php b/app/Commands/ZonesCommand.php index e1b83c4..b2c4f16 100644 --- a/app/Commands/ZonesCommand.php +++ b/app/Commands/ZonesCommand.php @@ -21,7 +21,8 @@ public function handle(): int $response = $connector->zones()->list($this->option('name')); if (! $response->successful()) { - $this->error('Failed to list zones: ' . $response->body()); + $this->error('Failed to list zones: '.$response->body()); + return self::FAILURE; } @@ -29,11 +30,13 @@ public function handle(): int if ($this->option('json')) { $this->line(json_encode($zones, JSON_PRETTY_PRINT)); + return self::SUCCESS; } if (empty($zones)) { $this->info('No zones found.'); + return self::SUCCESS; } diff --git a/app/Integrations/Cloudflare/CloudflareConnector.php b/app/Integrations/Cloudflare/CloudflareConnector.php index 9d7ade3..a57c16d 100644 --- a/app/Integrations/Cloudflare/CloudflareConnector.php +++ b/app/Integrations/Cloudflare/CloudflareConnector.php @@ -24,7 +24,7 @@ public function resolveBaseUrl(): string protected function defaultHeaders(): array { return [ - 'Authorization' => 'Bearer ' . $this->apiToken, + 'Authorization' => 'Bearer '.$this->apiToken, 'Content-Type' => 'application/json', ]; } diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000..a7a4845 --- /dev/null +++ b/bin/ci @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Local merge gate — same fail-closed set quality-runners runs on the PR. +# House PR quality is synapse-sentinel/quality-runners, not GitHub Actions. +set -euo pipefail +cd "$(dirname "$0")/.." + +echo "== Pint ==" +vendor/bin/pint --test + +echo "== PHPStan ==" +vendor/bin/phpstan analyse --no-progress --memory-limit=512M + +echo "== Rector ==" +vendor/bin/rector --dry-run --no-progress-bar + +echo "== Pest ==" +vendor/bin/pest --compact diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..fc94ae6 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,5 +1,7 @@ [ - App\Providers\AppServiceProvider::class, + AppServiceProvider::class, ], ]; diff --git a/config/commands.php b/config/commands.php index 8c45042..3a90bb4 100644 --- a/config/commands.php +++ b/config/commands.php @@ -1,5 +1,14 @@ NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, + 'default' => SummaryCommand::class, /* |-------------------------------------------------------------------------- @@ -55,14 +64,14 @@ */ 'hidden' => [ - NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, - Symfony\Component\Console\Command\DumpCompletionCommand::class, - Symfony\Component\Console\Command\HelpCommand::class, - Illuminate\Console\Scheduling\ScheduleRunCommand::class, - Illuminate\Console\Scheduling\ScheduleListCommand::class, - Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - Illuminate\Foundation\Console\VendorPublishCommand::class, - LaravelZero\Framework\Commands\StubPublishCommand::class, + SummaryCommand::class, + DumpCompletionCommand::class, + HelpCommand::class, + ScheduleRunCommand::class, + ScheduleListCommand::class, + ScheduleFinishCommand::class, + VendorPublishCommand::class, + StubPublishCommand::class, ], /* diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..dc42c4e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 5 + paths: + - app + treatPhpDocTypesAsCertain: false diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..6d32942 --- /dev/null +++ b/rector.php @@ -0,0 +1,12 @@ +withPaths([ + __DIR__.'/app', + __DIR__.'/tests', + ]) + ->withPhpSets(php83: true); diff --git a/tests/Feature/QualityRunnersCiTest.php b/tests/Feature/QualityRunnersCiTest.php new file mode 100644 index 0000000..5ae6842 --- /dev/null +++ b/tests/Feature/QualityRunnersCiTest.php @@ -0,0 +1,34 @@ +toContain('name: CI') + ->and($yml)->toContain('synapse-sentinel/quality-runners') + ->and($yml)->toContain('quality-runners is the house gate') + ->and($yml)->not->toContain('Pint + PHPStan + Pest') + ->and($yml)->not->toContain('actions/checkout') + ->and($yml)->not->toContain('setup-php') + ->and($yml)->not->toContain('./bin/ci') + ->and($yml)->not->toContain('vendor/bin/pint') + ->and($yml)->not->toContain('vendor/bin/phpstan') + ->and($yml)->not->toContain('vendor/bin/pest') + ->and($yml)->not->toContain('vendor/bin/rector'); +}); + +it('does not ship a synapse-sentinel/gate Actions workflow', function () { + $workflows = array_merge( + glob(base_path('.github/workflows/*.yml')) ?: [], + glob(base_path('.github/workflows/*.yaml')) ?: [], + ); + + expect($workflows)->not->toBeEmpty(); + + foreach ($workflows as $workflow) { + expect(file_get_contents($workflow)) + ->not->toContain('synapse-sentinel/gate') + ->and(basename($workflow))->not->toBe('gate.yml'); + } +}); diff --git a/tests/Pest.php b/tests/Pest.php index 68e2388..4c7c893 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@ in('Feature'); +uses(TestCase::class)->in('Feature'); /* |-------------------------------------------------------------------------- @@ -24,9 +26,7 @@ | */ -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); +expect()->extend('toBeOne', fn () => $this->toBe(1)); /* |-------------------------------------------------------------------------- diff --git a/tests/Unit/TunnelResourceTest.php b/tests/Unit/TunnelResourceTest.php index 21432a3..b5b0b2d 100644 --- a/tests/Unit/TunnelResourceTest.php +++ b/tests/Unit/TunnelResourceTest.php @@ -63,8 +63,6 @@ expect($response->successful())->toBeTrue() ->and($response->json('result.ok'))->toBeTrue(); - $mock->assertSent(function (Request $request) { - return $request instanceof UpdateTunnelConfiguration - && $request->resolveEndpoint() === '/accounts/acct/cfd_tunnel/tunnel-id/configurations'; - }); + $mock->assertSent(fn (Request $request) => $request instanceof UpdateTunnelConfiguration + && $request->resolveEndpoint() === '/accounts/acct/cfd_tunnel/tunnel-id/configurations'); }); From ead6b1c31682b2d8b7a9eb9199de9d16e95f1840 Mon Sep 17 00:00:00 2001 From: Jordan Lavar Partridge Date: Thu, 10 Sep 2026 06:14:45 -0700 Subject: [PATCH 2/3] fix: keep running Pest on Actions until quality-runners is subscribed The pointer job was green without executing tests. Residual GHA still runs Pint --test and Pest. It is not named quality-runners/gate. --- .github/workflows/ci.yml | 27 +++++++++++++++----------- README.md | 2 +- tests/Feature/QualityRunnersCiTest.php | 14 ++++--------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 242feea..309c4f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,23 +6,28 @@ on: push: branches: [main] -# Quality tools are not run on GitHub-hosted runners. -# synapse-sentinel/quality-runners clones the PR, runs Pint / Pest / PHPStan / Rector, -# and posts commit statuses (quality-runners/{tool}, quality-runners/gate). -# Merge still requires those green. Do not add a second runner here. +# House gate is synapse-sentinel/quality-runners (Pint / Pest / PHPStan / Rector +# on the clone host, then Statuses API: quality-runners/{tool} + quality-runners/gate). # https://github.com/synapse-sentinel/quality-runners +# +# Residual: this workflow still runs Pest (and Pint --test) on GitHub-hosted +# runners until conduit-ui is subscribed (quality-runners#73). It is not the +# house gate. Do not name this job "quality-runners is the house gate". concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - pointer: - name: quality-runners is the house gate + tests: runs-on: ubuntu-latest steps: - - name: Point at the house runner - run: | - echo "conduit-ui/cloudflare quality is synapse-sentinel/quality-runners, not this workflow." - echo "https://github.com/synapse-sentinel/quality-runners" - echo "This job does not run Pint, Pest, PHPStan, or Rector." + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, xml + coverage: none + - run: composer install --no-interaction --prefer-dist + - run: vendor/bin/pint --test + - run: vendor/bin/pest --compact diff --git a/README.md b/README.md index 87cc42a..904c125 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ app/ ## Development -PR quality is [quality-runners](https://github.com/synapse-sentinel/quality-runners) (Pint, Pest, PHPStan, Rector). GitHub Actions does not run those tools. +PR quality is [quality-runners](https://github.com/synapse-sentinel/quality-runners) (Pint, Pest, PHPStan, Rector on the clone host). Until that runner is subscribed to this repo, GitHub Actions still runs Pint `--test` and Pest as a residual. Local `./bin/ci` is the full set. Locally: diff --git a/tests/Feature/QualityRunnersCiTest.php b/tests/Feature/QualityRunnersCiTest.php index 5ae6842..13e1784 100644 --- a/tests/Feature/QualityRunnersCiTest.php +++ b/tests/Feature/QualityRunnersCiTest.php @@ -2,20 +2,14 @@ declare(strict_types=1); -it('points CI at quality-runners instead of running tools on GitHub Actions', function () { +it('does not fake-green a quality-runners gate check from GitHub Actions', function () { $yml = file_get_contents(base_path('.github/workflows/ci.yml')); expect($yml)->toContain('name: CI') ->and($yml)->toContain('synapse-sentinel/quality-runners') - ->and($yml)->toContain('quality-runners is the house gate') - ->and($yml)->not->toContain('Pint + PHPStan + Pest') - ->and($yml)->not->toContain('actions/checkout') - ->and($yml)->not->toContain('setup-php') - ->and($yml)->not->toContain('./bin/ci') - ->and($yml)->not->toContain('vendor/bin/pint') - ->and($yml)->not->toContain('vendor/bin/phpstan') - ->and($yml)->not->toContain('vendor/bin/pest') - ->and($yml)->not->toContain('vendor/bin/rector'); + ->and($yml)->toContain('vendor/bin/pest') + ->and($yml)->not->toContain('quality-runners is the house gate') + ->and($yml)->not->toContain('Pint + PHPStan + Pest'); }); it('does not ship a synapse-sentinel/gate Actions workflow', function () { From bb2e0c503853e603ed9025884d6263d2a1007e7a Mon Sep 17 00:00:00 2001 From: Jordan Lavar Partridge Date: Thu, 10 Sep 2026 06:15:06 -0700 Subject: [PATCH 3/3] test: forbid fake gate job name, not the residual comment --- tests/Feature/QualityRunnersCiTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/QualityRunnersCiTest.php b/tests/Feature/QualityRunnersCiTest.php index 13e1784..65011eb 100644 --- a/tests/Feature/QualityRunnersCiTest.php +++ b/tests/Feature/QualityRunnersCiTest.php @@ -8,7 +8,8 @@ expect($yml)->toContain('name: CI') ->and($yml)->toContain('synapse-sentinel/quality-runners') ->and($yml)->toContain('vendor/bin/pest') - ->and($yml)->not->toContain('quality-runners is the house gate') + ->and($yml)->toContain('vendor/bin/pint --test') + ->and($yml)->not->toContain('name: quality-runners is the house gate') ->and($yml)->not->toContain('Pint + PHPStan + Pest'); });