diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a28235a..309c4f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,22 @@ name: CI on: - push: - branches: [main, cursor/**] pull_request: + branches: [main] + push: + branches: [main] + +# 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: tests: @@ -16,4 +29,5 @@ jobs: extensions: mbstring, xml coverage: none - run: composer install --no-interaction --prefer-dist - - run: vendor/bin/pest + - run: vendor/bin/pint --test + - run: vendor/bin/pest --compact diff --git a/README.md b/README.md index a8df02e..904c125 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 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: + +```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..65011eb --- /dev/null +++ b/tests/Feature/QualityRunnersCiTest.php @@ -0,0 +1,29 @@ +toContain('name: CI') + ->and($yml)->toContain('synapse-sentinel/quality-runners') + ->and($yml)->toContain('vendor/bin/pest') + ->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'); +}); + +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'); });