Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
3.1 KiB
PHP
102 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Support\Health\Finding;
|
|
use App\Support\Health\HealthReport;
|
|
use App\Support\Health\Severity;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* The same checks as the panel page, for a deploy script.
|
|
*
|
|
* Exits non-zero when something is wrong, so a release can refuse to finish
|
|
* on a site that is already broken — or, with `--fail-on=error`, only on the
|
|
* faults that a visitor would actually hit.
|
|
*/
|
|
class SiteCheckCommand extends Command
|
|
{
|
|
protected $signature = 'site:check
|
|
{--fail-on=warning : Exit non-zero from this severity up (error|warning|never)}
|
|
{--json : Machine-readable output}';
|
|
|
|
protected $description = 'Check the public site for missing translations, broken URLs, broken images and other faults';
|
|
|
|
public function handle(HealthReport $report): int
|
|
{
|
|
// Every check: a deploy is not a person, so there is nobody here to
|
|
// hide a result from.
|
|
$findings = $report->runAll();
|
|
|
|
if ($this->option('json')) {
|
|
$this->line((string) json_encode($findings->map(fn (Finding $f) => [
|
|
'key' => $f->key,
|
|
'severity' => $f->severity->value,
|
|
'count' => $f->count,
|
|
'samples' => $f->samples,
|
|
])->all(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
return $this->exitCode($findings);
|
|
}
|
|
|
|
foreach ($findings as $finding) {
|
|
$this->components->twoColumnDetail(
|
|
$finding->title(),
|
|
$this->badge($finding),
|
|
);
|
|
|
|
foreach ($finding->samples as $sample) {
|
|
$this->line(" <fg=gray>· {$sample}</>");
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
|
|
$problems = $findings->filter(fn (Finding $f) => $f->severity->isProblem());
|
|
|
|
if ($problems->isEmpty()) {
|
|
$this->components->info(__('health.all_clear'));
|
|
} else {
|
|
$this->components->warn(
|
|
trans_choice('health.problem_summary', $problems->count(), ['count' => $problems->count()])
|
|
);
|
|
}
|
|
|
|
return $this->exitCode($findings);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Finding> $findings
|
|
*/
|
|
protected function exitCode($findings): int
|
|
{
|
|
$threshold = match ((string) $this->option('fail-on')) {
|
|
'error' => Severity::Error,
|
|
'never' => null,
|
|
default => Severity::Warning,
|
|
};
|
|
|
|
if ($threshold === null) {
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$failing = $findings->contains(
|
|
fn (Finding $f) => $f->severity->weight() <= $threshold->weight() && $f->severity->isProblem()
|
|
);
|
|
|
|
return $failing ? self::FAILURE : self::SUCCESS;
|
|
}
|
|
|
|
protected function badge(Finding $finding): string
|
|
{
|
|
return match ($finding->severity) {
|
|
Severity::Ok => '<fg=green>ok</>',
|
|
Severity::Warning => "<fg=yellow>{$finding->count}</>",
|
|
Severity::Error => "<fg=red>{$finding->count}</>",
|
|
};
|
|
}
|
|
}
|