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
Summary
Implement expressive review workflow API for approving, requesting changes, commenting, and managing PR reviews.
Acceptance Criteria
Review Builder Interface
Review Builder Implementation
Review Query
Review DTO
Usage Examples
Simple Approval
Request Changes with Inline Comments
Add Code Suggestions
Query Reviews
Automation: Auto-approve Dependabot
Testing Requirements
Dependencies
References
Labels