Skip to content

Reviews: Fluent review workflow API #22

@jordanpartridge

Description

@jordanpartridge

Summary

Implement expressive review workflow API for approving, requesting changes, commenting, and managing PR reviews.

Acceptance Criteria

Review Builder Interface

namespace ConduitUI\Pr\Contracts;

interface ReviewBuilderInterface
{
    public function approve(string $comment = null): self;
    public function requestChanges(string $comment = null): self;
    public function comment(string $body): self;
    public function addInlineComment(string $path, int $line, string $comment): self;
    public function addSuggestion(string $path, int $startLine, int $endLine, string $suggestion): self;
    public function submit(): Review;
}

Review Builder Implementation

namespace ConduitUI\Pr\Services;

use ConduitUI\Pr\Contracts\ReviewBuilderInterface;
use ConduitUI\Pr\Data\Review;

final class ReviewBuilder implements ReviewBuilderInterface
{
    protected ?string $event = null; // APPROVE | REQUEST_CHANGES | COMMENT
    protected ?string $body = null;
    protected array $comments = [];
    
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
        protected int $prNumber,
    ) {}
    
    public function approve(string $comment = null): self
    {
        $this->event = 'APPROVE';
        $this->body = $comment;
        return $this;
    }
    
    public function requestChanges(string $comment = null): self
    {
        $this->event = 'REQUEST_CHANGES';
        $this->body = $comment ?? 'Changes requested';
        return $this;
    }
    
    public function comment(string $body): self
    {
        $this->event = 'COMMENT';
        $this->body = $body;
        return $this;
    }
    
    public function addInlineComment(
        string $path,
        int $line,
        string $comment
    ): self {
        $this->comments[] = [
            'path' => $path,
            'line' => $line,
            'body' => $comment,
        ];
        return $this;
    }
    
    public function addSuggestion(
        string $path,
        int $startLine,
        int $endLine,
        string $suggestion
    ): self {
        $this->comments[] = [
            'path' => $path,
            'start_line' => $startLine,
            'line' => $endLine,
            'body' => "```suggestion\n{$suggestion}\n```",
        ];
        return $this;
    }
    
    public function submit(): Review
    {
        $response = $this->github->post(
            "/repos/{$this->fullName}/pulls/{$this->prNumber}/reviews",
            [
                'event' => $this->event,
                'body' => $this->body,
                'comments' => $this->comments,
            ]
        );
        
        return Review::fromArray($response->json());
    }
}

Review Query

namespace ConduitUI\Pr\Services;

final class ReviewQuery
{
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
        protected int $prNumber,
    ) {}
    
    public function get(): Collection
    {
        $response = $this->github->get(
            "/repos/{$this->fullName}/pulls/{$this->prNumber}/reviews"
        );
        
        return collect($response->json())
            ->map(fn($review) => Review::fromArray($review));
    }
    
    public function whereApproved(): Collection
    {
        return $this->get()->where('state', 'APPROVED');
    }
    
    public function whereChangesRequested(): Collection
    {
        return $this->get()->where('state', 'CHANGES_REQUESTED');
    }
    
    public function wherePending(): Collection
    {
        return $this->get()->where('state', 'PENDING');
    }
    
    public function byUser(string $username): Collection
    {
        return $this->get()->where('user.login', $username);
    }
    
    public function latest(): ?Review
    {
        return $this->get()->sortByDesc('submitted_at')->first();
    }
}

Review DTO

namespace ConduitUI\Pr\Data;

final readonly class Review
{
    public function __construct(
        public int $id,
        public User $user,
        public string $state, // APPROVED | CHANGES_REQUESTED | COMMENTED | PENDING
        public ?string $body,
        public string $htmlUrl,
        public Carbon $submittedAt,
    ) {}
    
    public static function fromArray(array $data): self;
    
    public function isApproved(): bool
    {
        return $this->state === 'APPROVED';
    }
    
    public function isChangesRequested(): bool
    {
        return $this->state === 'CHANGES_REQUESTED';
    }
    
    public function isPending(): bool
    {
        return $this->state === 'PENDING';
    }
}

Usage Examples

Simple Approval

PullRequests::find('owner/repo', 123)
    ->approve('LGTM! Great work.')
    ->submit();

Request Changes with Inline Comments

PullRequests::find('owner/repo', 456)
    ->requestChanges('Please address the following concerns:')
    ->addInlineComment('src/Service.php', 42, 'This could cause a race condition')
    ->addInlineComment('tests/ServiceTest.php', 15, 'Missing edge case test')
    ->submit();

Add Code Suggestions

PullRequests::find('owner/repo', 789)
    ->comment('Few suggestions for improvement')
    ->addSuggestion('src/Controller.php', 20, 22, 
        'return $this->repository->findOrFail($id);'
    )
    ->submit();

Query Reviews

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

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

// Filter reviews
$approvals = $pr->reviews()->whereApproved();
$changesRequested = $pr->reviews()->whereChangesRequested();

// Get latest review
$latest = $pr->reviews()->latest();

// Check if specific user approved
$userApproved = $pr->reviews()
    ->byUser('senior-dev')
    ->whereApproved()
    ->isNotEmpty();

Automation: Auto-approve Dependabot

PullRequests::forRepo('owner/repo')
    ->whereOpen()
    ->whereAuthor('dependabot[bot]')
    ->get()
    ->filter(fn($pr) => $pr->checksPass())
    ->each(fn($pr) => 
        $pr->approve('Auto-approving passing dependency update')
           ->submit()
    );

Testing Requirements

it('submits approval review')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)
            ->approve('LGTM')
            ->submit()
    )->toBeInstanceOf(Review::class);

it('submits changes requested with comments')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)
            ->requestChanges('Please fix')
            ->addInlineComment('src/File.php', 10, 'Issue here')
            ->submit()
    )->toBeInstanceOf(Review::class);

it('queries approved reviews')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)
            ->reviews()
            ->whereApproved()
    )->toBeInstanceOf(Collection::class);

Dependencies

References

Labels

  • enhancement
  • reviews
  • workflow

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