Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/Branding.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ final class Branding
{
// Check names (sorted by display order)
public const TESTS = '① Tests & Coverage';

public const SECURITY = '② Security Audit';

public const SYNTAX = '③ Pest Syntax';

public const CERTIFICATION = '🏆 Sentinel Certified';

// Map internal names to branded names
Expand Down
1 change: 1 addition & 0 deletions app/Checks/CheckInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
interface CheckInterface
{
public function name(): string;

public function run(string $workingDirectory): CheckResult;
}
8 changes: 4 additions & 4 deletions app/Checks/PestSyntaxValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ public function name(): string

public function run(string $workingDirectory): CheckResult
{
$testsPath = $workingDirectory . '/tests';
$testsPath = $workingDirectory.'/tests';

if (!is_dir($testsPath)) {
if (! is_dir($testsPath)) {
return CheckResult::pass('No tests directory found');
}

$finder = new Finder();
$finder = new Finder;
$finder->files()->in($testsPath)->name('*Test.php');

$violations = [];
Expand All @@ -31,7 +31,7 @@ public function run(string $workingDirectory): CheckResult

// Check for test() function usage (but not in comments)
if (preg_match('/^\s*test\s*\(/m', $contents)) {
$violations[] = $file->getRelativePathname() . ': Uses test() instead of describe/it blocks';
$violations[] = $file->getRelativePathname().': Uses test() instead of describe/it blocks';
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/Checks/SecurityScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
final class SecurityScanner implements CheckInterface
{
public function __construct(
private readonly ProcessRunner $processRunner = new SymfonyProcessRunner(),
private readonly ProcessRunner $processRunner = new SymfonyProcessRunner,
) {}

public function name(): string
Expand Down
6 changes: 3 additions & 3 deletions app/Checks/TestRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ final class TestRunner implements CheckInterface

public function __construct(
private readonly int $coverageThreshold = 100,
private readonly PestOutputParser $parser = new PestOutputParser(),
private readonly ProcessRunner $processRunner = new SymfonyProcessRunner(),
private readonly PestOutputParser $parser = new PestOutputParser,
private readonly ProcessRunner $processRunner = new SymfonyProcessRunner,
) {}

/** @internal For testing only */
Expand All @@ -40,7 +40,7 @@ public function name(): string

public function run(string $workingDirectory): CheckResult
{
$cloverPath = $workingDirectory . '/coverage.xml';
$cloverPath = $workingDirectory.'/coverage.xml';

$result = $this->processRunner->run(
['vendor/bin/pest', '--coverage', "--min={$this->coverageThreshold}", "--coverage-clover={$cloverPath}", '--colors=never'],
Expand Down
17 changes: 13 additions & 4 deletions app/Commands/CertifyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ public function handle(): int

$checks = $this->checks ?? [
new TestRunner($coverageThreshold),
new SecurityScanner(),
new PestSyntaxValidator(),
new SecurityScanner,
new PestSyntaxValidator,
];

$stopOnFailure = $this->option('stop-on-failure');
Expand Down Expand Up @@ -100,7 +100,7 @@ public function handle(): int
// Report Sentinel Certification (last, after all checks)
$title = $verdict->isApproved()
? 'All checks passed'
: count($failures) . ' check(s) failed';
: count($failures).' check(s) failed';

$checksClient->reportCheck(
name: Branding::CERTIFICATION,
Expand All @@ -117,6 +117,15 @@ public function handle(): int
// Output verdict
if ($compact) {
$this->renderCompactOutput($compactResults, $verdict);

// Still show failure details in compact mode when checks fail
if (! $verdict->isApproved() && ! empty($failureRows)) {
echo "\n";
table(
headers: ['Check', 'Issue'],
rows: $failureRows
);
}
} elseif ($verdict->isApproved()) {
info('');
info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
Expand Down Expand Up @@ -206,7 +215,7 @@ private function renderCompactOutput(array $results, Verdict $verdict): void
}

$status = $verdict->isApproved() ? '✓ APPROVED' : '✗ REJECTED';
$line = implode(' ', $parts) . " │ {$status}";
$line = implode(' ', $parts)." │ {$status}";

if ($verdict->isApproved()) {
info($line);
Expand Down
2 changes: 1 addition & 1 deletion app/Commands/SecurityCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function handle(): int
{
$token = $this->option('token') ?: getenv('GITHUB_TOKEN') ?: null;
$checksClient = $this->checksClient ?? new ChecksClient($token);
$check = $this->check ?? new SecurityScanner();
$check = $this->check ?? new SecurityScanner;

$result = $check->run(getcwd());

Expand Down
2 changes: 1 addition & 1 deletion app/Commands/SyntaxCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function handle(): int
{
$token = $this->option('token') ?: getenv('GITHUB_TOKEN') ?: null;
$checksClient = $this->checksClient ?? new ChecksClient($token);
$check = $this->check ?? new PestSyntaxValidator();
$check = $this->check ?? new PestSyntaxValidator;

$result = $check->run(getcwd());

Expand Down
2 changes: 2 additions & 0 deletions app/GitHub/ChecksClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public function reportCheck(
if (! $this->isAvailable()) {
$hasToken = $this->token ? 'yes' : 'no';
echo "::warning::ChecksClient not available (token={$hasToken}, repo={$this->repo}, sha={$this->sha})\n";

return false;
}

Expand All @@ -152,6 +153,7 @@ public function reportCheck(
return true;
} catch (GuzzleException $e) {
echo "::warning::ChecksClient error: {$e->getMessage()}\n";

return false;
}
}
Expand Down
2 changes: 2 additions & 0 deletions app/GitHub/CommentsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public function postOrUpdateComment(string $body, string $signature = '🏆 Syna
return true;
} catch (GuzzleException $e) {
echo "::warning::Failed to post/update PR comment: {$e->getMessage()}\n";

return false;
}
}
Expand Down Expand Up @@ -106,6 +107,7 @@ private function extractPRNumber(): ?int
}

$event = json_decode(file_get_contents($eventPath), true);

return $event['pull_request']['number'] ?? null;
}
}
4 changes: 2 additions & 2 deletions app/Services/CoverageReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function parseClover(string $path): array

$metrics = $xml->project->metrics ?? null;
if ($metrics === null) {
throw new \RuntimeException("Invalid clover format: missing project metrics");
throw new \RuntimeException('Invalid clover format: missing project metrics');
}

$files = [];
Expand Down Expand Up @@ -127,7 +127,7 @@ private function formatMarkdown(array $current, ?array $base): string
$uncoveredCount = count($file['uncovered_lines']);
$uncoveredPreview = $uncoveredCount > 0 ? implode(', ', array_slice($file['uncovered_lines'], 0, 5)) : 'None';
if ($uncoveredCount > 5) {
$uncoveredPreview .= "... (+".($uncoveredCount - 5)." more)";
$uncoveredPreview .= '... (+'.($uncoveredCount - 5).' more)';
}
$markdown .= "| `{$fileName}` | {$coverage}% | {$uncoveredPreview} |\n";
}
Expand Down
2 changes: 1 addition & 1 deletion app/Services/PestOutputParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private function formatFailure(string $name, string $body): string
$location = preg_match('/at\s+(\S+:\d+)/', $body, $m) ? " ({$m[1]})" : '';
$message = $this->extractAssertionMessage($body);

return $name . $location . $message;
return $name.$location.$message;
}

private function extractAssertionMessage(string $body): string
Expand Down
2 changes: 1 addition & 1 deletion app/Stages/TechnicalGate.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
final class TechnicalGate
{
/**
* @param array<CheckInterface> $checks
* @param array<CheckInterface> $checks
*/
public function __construct(
private readonly array $checks,
Expand Down
5 changes: 2 additions & 3 deletions app/Verdict.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
final readonly class Verdict
{
/**
* @param array<int, string> $failures
* @param array<int, string> $failures
*/
private function __construct(
private string $status,
private string $reason,
private array $failures = []
) {
}
) {}

public static function approved(string $reason): self
{
Expand Down
22 changes: 11 additions & 11 deletions tests/Feature/RunCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,69 +10,69 @@
describe('Gate Commands', function () {
describe('TestsCommand', function () {
it('has the correct signature', function () {
$command = new TestsCommand();
$command = new TestsCommand;
expect($command->getName())->toBe('tests');
});

it('has coverage option with default of 80', function () {
$command = new TestsCommand();
$command = new TestsCommand;
$definition = $command->getDefinition();

expect($definition->hasOption('coverage'))->toBeTrue();
expect($definition->getOption('coverage')->getDefault())->toBe('80');
});

it('has token option', function () {
$command = new TestsCommand();
$command = new TestsCommand;
expect($command->getDefinition()->hasOption('token'))->toBeTrue();
});
});

describe('SecurityCommand', function () {
it('has the correct signature', function () {
$command = new SecurityCommand();
$command = new SecurityCommand;
expect($command->getName())->toBe('security');
});

it('has token option', function () {
$command = new SecurityCommand();
$command = new SecurityCommand;
expect($command->getDefinition()->hasOption('token'))->toBeTrue();
});
});

describe('SyntaxCommand', function () {
it('has the correct signature', function () {
$command = new SyntaxCommand();
$command = new SyntaxCommand;
expect($command->getName())->toBe('syntax');
});

it('has token option', function () {
$command = new SyntaxCommand();
$command = new SyntaxCommand;
expect($command->getDefinition()->hasOption('token'))->toBeTrue();
});
});

describe('CertifyCommand', function () {
it('has the correct signature', function () {
$command = new CertifyCommand();
$command = new CertifyCommand;
expect($command->getName())->toBe('certify');
});

it('has coverage option with default of 80', function () {
$command = new CertifyCommand();
$command = new CertifyCommand;
$definition = $command->getDefinition();

expect($definition->hasOption('coverage'))->toBeTrue();
expect($definition->getOption('coverage')->getDefault())->toBe('80');
});

it('has token option', function () {
$command = new CertifyCommand();
$command = new CertifyCommand;
expect($command->getDefinition()->hasOption('token'))->toBeTrue();
});

it('has compact option', function () {
$command = new CertifyCommand();
$command = new CertifyCommand;
$definition = $command->getDefinition();

expect($definition->hasOption('compact'))->toBeTrue();
Expand Down
Loading