Skip to content

Checks: CI/CD status and check runs integration #23

@jordanpartridge

Description

@jordanpartridge

Summary

Implement check runs and status API for querying CI/CD pipeline results and PR status checks.

Acceptance Criteria

Check Run Query Interface

namespace ConduitUI\Pr\Contracts;

interface CheckRunQueryInterface
{
    public function get(): Collection;
    public function wherePassing(): Collection;
    public function whereFailing(): Collection;
    public function wherePending(): Collection;
    public function whereNeutral(): Collection;
    public function whereSkipped(): Collection;
    public function latest(): ?CheckRun;
    public function byName(string $name): ?CheckRun;
    public function summary(): CheckSummary;
}

Check Run Query Implementation

namespace ConduitUI\Pr\Services;

use ConduitUI\Pr\Contracts\CheckRunQueryInterface;
use ConduitUI\Pr\Data\CheckRun;
use ConduitUI\Pr\Data\CheckSummary;

final class CheckRunQuery implements CheckRunQueryInterface
{
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
        protected string $ref, // SHA or branch name
    ) {}
    
    public function get(): Collection
    {
        $response = $this->github->get(
            "/repos/{$this->fullName}/commits/{$this->ref}/check-runs"
        );
        
        return collect($response->json('check_runs'))
            ->map(fn($check) => CheckRun::fromArray($check));
    }
    
    public function wherePassing(): Collection
    {
        return $this->get()
            ->where('conclusion', 'success');
    }
    
    public function whereFailing(): Collection
    {
        return $this->get()
            ->whereIn('conclusion', ['failure', 'timed_out', 'action_required']);
    }
    
    public function wherePending(): Collection
    {
        return $this->get()
            ->whereNull('conclusion')
            ->where('status', 'in_progress');
    }
    
    public function whereNeutral(): Collection
    {
        return $this->get()
            ->where('conclusion', 'neutral');
    }
    
    public function whereSkipped(): Collection
    {
        return $this->get()
            ->where('conclusion', 'skipped');
    }
    
    public function latest(): ?CheckRun
    {
        return $this->get()
            ->sortByDesc('completed_at')
            ->first();
    }
    
    public function byName(string $name): ?CheckRun
    {
        return $this->get()
            ->firstWhere('name', $name);
    }
    
    public function summary(): CheckSummary
    {
        $checks = $this->get();
        
        return new CheckSummary(
            total: $checks->count(),
            passing: $checks->where('conclusion', 'success')->count(),
            failing: $checks->whereIn('conclusion', ['failure', 'timed_out'])->count(),
            pending: $checks->whereNull('conclusion')->count(),
            neutral: $checks->where('conclusion', 'neutral')->count(),
            skipped: $checks->where('conclusion', 'skipped')->count(),
        );
    }
}

Check Run DTO

namespace ConduitUI\Pr\Data;

final readonly class CheckRun
{
    public function __construct(
        public int $id,
        public string $name,
        public string $status, // queued | in_progress | completed
        public ?string $conclusion, // success | failure | neutral | cancelled | skipped | timed_out | action_required
        public string $headSha,
        public ?string $detailsUrl,
        public ?string $htmlUrl,
        public ?Carbon $startedAt,
        public ?Carbon $completedAt,
    ) {}
    
    public static function fromArray(array $data): self;
    
    public function isPassing(): bool
    {
        return $this->conclusion === 'success';
    }
    
    public function isFailing(): bool
    {
        return in_array($this->conclusion, ['failure', 'timed_out', 'action_required']);
    }
    
    public function isPending(): bool
    {
        return $this->status === 'in_progress' && $this->conclusion === null;
    }
    
    public function isCompleted(): bool
    {
        return $this->status === 'completed';
    }
}

Check Summary DTO

namespace ConduitUI\Pr\Data;

final readonly class CheckSummary
{
    public function __construct(
        public int $total,
        public int $passing,
        public int $failing,
        public int $pending,
        public int $neutral,
        public int $skipped,
    ) {}
    
    public function allPassing(): bool
    {
        return $this->total > 0 
            && $this->failing === 0 
            && $this->pending === 0;
    }
    
    public function anyFailing(): bool
    {
        return $this->failing > 0;
    }
    
    public function isComplete(): bool
    {
        return $this->pending === 0;
    }
    
    public function passRate(): float
    {
        if ($this->total === 0) {
            return 0;
        }
        
        return round(($this->passing / $this->total) * 100, 2);
    }
}

Integration with PullRequest DTO

// Add to PullRequest DTO
public function checks(): CheckRunQuery
{
    return new CheckRunQuery(
        $this->github,
        $this->fullName,
        $this->headSha
    );
}

public function checksPass(): bool
{
    return $this->checks()->summary()->allPassing();
}

public function checksFail(): bool
{
    return $this->checks()->summary()->anyFailing();
}

public function checksPending(): bool
{
    return !$this->checks()->summary()->isComplete();
}

Usage Examples

Query All Checks

$pr = PullRequests::find('owner/repo', 123);

// Get all checks
$checks = $pr->checks()->get();

// Filter checks
$passing = $pr->checks()->wherePassing();
$failing = $pr->checks()->whereFailing();
$pending = $pr->checks()->wherePending();

// Get specific check
$ciCheck = $pr->checks()->byName('CI');

Check Summary

$summary = $pr->checks()->summary();

echo "Total: {$summary->total}";
echo "Passing: {$summary->passing}";
echo "Failing: {$summary->failing}";
echo "Pass Rate: {$summary->passRate()}%";

if ($summary->allPassing()) {
    echo "All checks passing!";
}

Conditional Operations Based on Checks

$pr = PullRequests::find('owner/repo', 123);

if ($pr->checksPass()) {
    $pr->approve('Checks passing, approving')->submit();
}

if ($pr->checksFail()) {
    $failing = $pr->checks()->whereFailing();
    $message = "Failing checks:\n";
    foreach ($failing as $check) {
        $message .= "- {$check->name}\n";
    }
    $pr->comment($message);
}

Wait for Checks to Complete

use Illuminate\Support\Sleep;

$pr = PullRequests::find('owner/repo', 123);

while ($pr->checksPending()) {
    Sleep::for(30)->seconds();
    $pr = $pr->fresh(); // Refresh PR data
}

if ($pr->checksPass()) {
    $pr->merge();
}

Auto-merge When Checks Pass

PullRequests::forRepo('owner/repo')
    ->whereOpen()
    ->whereLabel('auto-merge')
    ->get()
    ->filter(fn($pr) => $pr->checksPass() && $pr->isApproved())
    ->each(fn($pr) => $pr->merge());

Testing Requirements

it('gets all check runs')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->checks()->get()
    )->toBeInstanceOf(Collection::class);

it('filters passing checks')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->checks()->wherePassing()
    )->toBeInstanceOf(Collection::class);

it('generates check summary')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->checks()->summary()
    )->toBeInstanceOf(CheckSummary::class);

it('determines if all checks pass')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->checksPass()
    )->toBeBool();

Dependencies

References

Labels

  • enhancement
  • checks
  • ci-cd

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions