Skip to content

Creation: Fluent PR creation builder API #26

@jordanpartridge

Description

@jordanpartridge

Summary

Implement expressive fluent API for creating pull requests with all configuration options.

Acceptance Criteria

PR Creation Builder Interface

namespace ConduitUI\Pr\Contracts;

interface PullRequestBuilderInterface
{
    public function title(string $title): self;
    public function body(string $body): self;
    public function head(string $branch): self;
    public function base(string $branch): self;
    public function draft(bool $draft = true): self;
    public function maintainerCanModify(bool $allowed = true): self;
    public function create(): PullRequest;
}

PR Creation Builder Implementation

namespace ConduitUI\Pr\Services;

use ConduitUI\Pr\Contracts\PullRequestBuilderInterface;
use ConduitUI\Pr\Data\PullRequest;

final class PullRequestBuilder implements PullRequestBuilderInterface
{
    protected ?string $title = null;
    protected ?string $body = null;
    protected ?string $head = null;
    protected string $base = 'main';
    protected bool $draft = false;
    protected bool $maintainerCanModify = true;
    
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
    ) {}
    
    public function title(string $title): self
    {
        $this->title = $title;
        return $this;
    }
    
    public function body(string $body): self
    {
        $this->body = $body;
        return $this;
    }
    
    public function head(string $branch): self
    {
        $this->head = $branch;
        return $this;
    }
    
    public function base(string $branch): self
    {
        $this->base = $branch;
        return $this;
    }
    
    public function draft(bool $draft = true): self
    {
        $this->draft = $draft;
        return $this;
    }
    
    public function maintainerCanModify(bool $allowed = true): self
    {
        $this->maintainerCanModify = $allowed;
        return $this;
    }
    
    public function create(): PullRequest
    {
        if ($this->title === null) {
            throw new \InvalidArgumentException('Title is required');
        }
        
        if ($this->head === null) {
            throw new \InvalidArgumentException('Head branch is required');
        }
        
        $data = [
            'title' => $this->title,
            'head' => $this->head,
            'base' => $this->base,
            'draft' => $this->draft,
            'maintainer_can_modify' => $this->maintainerCanModify,
        ];
        
        if ($this->body !== null) {
            $data['body'] = $this->body;
        }
        
        $response = $this->github->post(
            "/repos/{$this->fullName}/pulls",
            $data
        );
        
        return PullRequest::fromArray($response->json());
    }
}

Post-Creation Actions Builder

final class PullRequestPostActions
{
    protected array $labels = [];
    protected array $reviewers = [];
    protected array $teamReviewers = [];
    protected array $assignees = [];
    
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
        protected PullRequest $pullRequest,
    ) {}
    
    public function labels(array $labels): self
    {
        $this->labels = $labels;
        return $this;
    }
    
    public function reviewers(array $usernames): self
    {
        $this->reviewers = $usernames;
        return $this;
    }
    
    public function teamReviewers(array $teamSlugs): self
    {
        $this->teamReviewers = $teamSlugs;
        return $this;
    }
    
    public function assignees(array $usernames): self
    {
        $this->assignees = $usernames;
        return $this;
    }
    
    public function apply(): PullRequest
    {
        if (!empty($this->labels)) {
            $this->github->post(
                "/repos/{$this->fullName}/issues/{$this->pullRequest->number}/labels",
                ['labels' => $this->labels]
            );
        }
        
        if (!empty($this->reviewers) || !empty($this->teamReviewers)) {
            $this->github->post(
                "/repos/{$this->fullName}/pulls/{$this->pullRequest->number}/requested_reviewers",
                [
                    'reviewers' => $this->reviewers,
                    'team_reviewers' => $this->teamReviewers,
                ]
            );
        }
        
        if (!empty($this->assignees)) {
            $this->github->post(
                "/repos/{$this->fullName}/issues/{$this->pullRequest->number}/assignees",
                ['assignees' => $this->assignees]
            );
        }
        
        // Refresh and return updated PR
        return PullRequests::find($this->fullName, $this->pullRequest->number)->fresh();
    }
}

Facade Integration

// Add to PullRequests facade/service
public function create(): PullRequestBuilder
{
    return new PullRequestBuilder($this->github, $this->fullName);
}

Usage Examples

Simple PR Creation

$pr = PullRequests::forRepo('owner/repo')
    ->create()
    ->title('feat: Add user authentication')
    ->body('This PR implements JWT-based authentication')
    ->head('feature/auth')
    ->base('main')
    ->create();

Draft PR

$pr = PullRequests::forRepo('owner/repo')
    ->create()
    ->title('WIP: Refactor database layer')
    ->body('Work in progress, do not merge')
    ->head('refactor/database')
    ->draft()
    ->create();

PR with Reviewers and Labels (Chained)

$pr = PullRequests::forRepo('owner/repo')
    ->create()
    ->title('fix: Resolve race condition in checkout')
    ->body('Fixes #123')
    ->head('bugfix/checkout-race')
    ->base('develop')
    ->create();

// Add metadata post-creation
$pr->addLabels(['bug', 'high-priority'])
   ->requestReviews(['senior-dev', 'team-lead'])
   ->assign('jordan');

Complete PR Setup

$pr = PullRequests::forRepo('owner/repo')
    ->create()
    ->title('feat: Add payment processing')
    ->body(<<<MD
        ## Changes
        - Stripe integration
        - Payment webhooks
        - Refund handling
        
        ## Testing
        - [ ] Unit tests passing
        - [ ] Integration tests passing
        - [ ] Manual testing completed
        MD)
    ->head('feature/payments')
    ->base('main')
    ->create();

// Configure post-creation
$pr->requestReviews(['backend-lead', 'security-team'])
   ->requestTeamReview('payments-team')
   ->addLabels(['feature', 'payments'])
   ->assign('jordan');

Automated PR from CI

// Auto-create dependency update PR
$branch = 'deps/update-laravel-11';

$pr = PullRequests::forRepo('owner/repo')
    ->create()
    ->title('chore: Update Laravel to 11.x')
    ->body('Automated dependency update')
    ->head($branch)
    ->base('main')
    ->draft()
    ->create();

// Wait for checks
while ($pr->checksPending()) {
    sleep(30);
    $pr = $pr->fresh();
}

// Auto-approve if passing
if ($pr->checksPass()) {
    $pr->markReady()
       ->approve('Automated approval for passing dependency update')
       ->submit()
       ->merge();
}

Cross-Repository PR (Fork)

$pr = PullRequests::forRepo('upstream/repo')
    ->create()
    ->title('feat: Add dark mode')
    ->body('Contributes dark mode support')
    ->head('myusername:feature/dark-mode') // from fork
    ->base('main')
    ->maintainerCanModify(true)
    ->create();

Template-Based PR

$template = file_get_contents('.github/PULL_REQUEST_TEMPLATE.md');

$pr = PullRequests::forRepo('owner/repo')
    ->create()
    ->title('feat: New feature')
    ->body($template)
    ->head('feature/new')
    ->create();

Bulk PR Creation

$branches = ['feature/one', 'feature/two', 'feature/three'];

foreach ($branches as $branch) {
    PullRequests::forRepo('owner/repo')
        ->create()
        ->title("Feature: {$branch}")
        ->body("Auto-generated PR for {$branch}")
        ->head($branch)
        ->base('develop')
        ->draft()
        ->create();
}

Testing Requirements

it('creates a simple PR')
    ->expect(fn() => 
        PullRequests::forRepo('test/repo')
            ->create()
            ->title('Test PR')
            ->head('test-branch')
            ->create()
    )->toBeInstanceOf(PullRequest::class);

it('creates draft PR')
    ->expect(fn() => 
        PullRequests::forRepo('test/repo')
            ->create()
            ->title('Draft PR')
            ->head('draft-branch')
            ->draft()
            ->create()
    )->toBeInstanceOf(PullRequest::class);

it('requires title')
    ->expect(fn() => 
        PullRequests::forRepo('test/repo')
            ->create()
            ->head('branch')
            ->create()
    )->toThrow(InvalidArgumentException::class);

it('requires head branch')
    ->expect(fn() => 
        PullRequests::forRepo('test/repo')
            ->create()
            ->title('Test')
            ->create()
    )->toThrow(InvalidArgumentException::class);

Dependencies

References

Labels

  • enhancement
  • creation
  • builder

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