Skip to content

Labels: Fluent label management API #30

@jordanpartridge

Description

@jordanpartridge

Summary

Implement expressive API for managing PR labels including add, remove, replace, and label-based automation.

Acceptance Criteria

Label Manager Interface

namespace ConduitUI\Pr\Contracts;

use ConduitUI\Pr\Data\Label;
use Illuminate\Support\Collection;

interface LabelManagerInterface
{
    public function get(): Collection;
    public function add(string $label): self;
    public function addMany(array $labels): self;
    public function remove(string $label): self;
    public function removeMany(array $labels): self;
    public function replace(array $labels): self;
    public function clear(): self;
    public function has(string $label): bool;
    public function hasAny(array $labels): bool;
    public function hasAll(array $labels): bool;
}

Label Manager Implementation

namespace ConduitUI\Pr\Services;

use ConduitUI\Pr\Contracts\LabelManagerInterface;
use ConduitUI\Pr\Data\Label;
use ConduitUI\Connector\GitHub;
use Illuminate\Support\Collection;

final class LabelManager implements LabelManagerInterface
{
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
        protected int $prNumber,
    ) {}
    
    public function get(): Collection
    {
        $response = $this->github->get(
            "/repos/{$this->fullName}/issues/{$this->prNumber}/labels"
        );
        
        return collect($response->json())
            ->map(fn($label) => Label::fromArray($label));
    }
    
    public function add(string $label): self
    {
        return $this->addMany([$label]);
    }
    
    public function addMany(array $labels): self
    {
        $this->github->post(
            "/repos/{$this->fullName}/issues/{$this->prNumber}/labels",
            ['labels' => $labels]
        );
        
        return $this;
    }
    
    public function remove(string $label): self
    {
        $this->github->delete(
            "/repos/{$this->fullName}/issues/{$this->prNumber}/labels/{$label}"
        );
        
        return $this;
    }
    
    public function removeMany(array $labels): self
    {
        foreach ($labels as $label) {
            $this->remove($label);
        }
        
        return $this;
    }
    
    public function replace(array $labels): self
    {
        $this->github->put(
            "/repos/{$this->fullName}/issues/{$this->prNumber}/labels",
            ['labels' => $labels]
        );
        
        return $this;
    }
    
    public function clear(): self
    {
        $this->github->delete(
            "/repos/{$this->fullName}/issues/{$this->prNumber}/labels"
        );
        
        return $this;
    }
    
    public function has(string $label): bool
    {
        return $this->get()
            ->contains('name', $label);
    }
    
    public function hasAny(array $labels): bool
    {
        $currentLabels = $this->get()->pluck('name');
        
        return collect($labels)->intersect($currentLabels)->isNotEmpty();
    }
    
    public function hasAll(array $labels): bool
    {
        $currentLabels = $this->get()->pluck('name');
        
        return collect($labels)->diff($currentLabels)->isEmpty();
    }
}

Repository Label Manager

namespace ConduitUI\Pr\Services;

use ConduitUI\Pr\Data\Label;
use ConduitUI\Connector\GitHub;
use Illuminate\Support\Collection;

/**
 * Manage labels at repository level
 */
final class RepositoryLabelManager
{
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
    ) {}
    
    public function get(): Collection
    {
        $response = $this->github->get(
            "/repos/{$this->fullName}/labels"
        );
        
        return collect($response->json())
            ->map(fn($label) => Label::fromArray($label));
    }
    
    public function create(
        string $name,
        string $color,
        string $description = ''
    ): Label {
        $response = $this->github->post(
            "/repos/{$this->fullName}/labels",
            [
                'name' => $name,
                'color' => $color,
                'description' => $description,
            ]
        );
        
        return Label::fromArray($response->json());
    }
    
    public function update(
        string $name,
        ?string $newName = null,
        ?string $color = null,
        ?string $description = null
    ): Label {
        $data = array_filter([
            'new_name' => $newName,
            'color' => $color,
            'description' => $description,
        ]);
        
        $response = $this->github->patch(
            "/repos/{$this->fullName}/labels/{$name}",
            $data
        );
        
        return Label::fromArray($response->json());
    }
    
    public function delete(string $name): bool
    {
        $response = $this->github->delete(
            "/repos/{$this->fullName}/labels/{$name}"
        );
        
        return $response->successful();
    }
}

Label DTO

namespace ConduitUI\Pr\Data;

final readonly class Label
{
    public function __construct(
        public int $id,
        public string $name,
        public string $color,
        public string $description,
        public bool $default,
    ) {}
    
    public static function fromArray(array $data): self
    {
        return new self(
            id: $data['id'],
            name: $data['name'],
            color: $data['color'],
            description: $data['description'] ?? '',
            default: $data['default'] ?? false,
        );
    }
    
    public function hexColor(): string
    {
        return "#{$this->color}";
    }
}

Integration with PullRequestInstance

// Add to PullRequestInstance
public function labels(): LabelManager
{
    return new LabelManager($this->github, $this->fullName, $this->number);
}

public function addLabel(string $label): self
{
    $this->labels()->add($label);
    return $this;
}

public function addLabels(array $labels): self
{
    $this->labels()->addMany($labels);
    return $this;
}

public function removeLabel(string $label): self
{
    $this->labels()->remove($label);
    return $this;
}

public function setLabels(array $labels): self
{
    $this->labels()->replace($labels);
    return $this;
}

Usage Examples

Basic Label Operations

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

// Add single label
$pr->addLabel('bug');

// Add multiple labels
$pr->addLabels(['bug', 'high-priority', 'needs-review']);

// Remove label
$pr->removeLabel('needs-review');

// Replace all labels
$pr->setLabels(['reviewed', 'ready-to-merge']);

// Clear all labels
$pr->labels()->clear();

Checking Labels

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

// Check if has label
if ($pr->labels()->has('auto-merge')) {
    $pr->merge();
}

// Check if has any of these labels
if ($pr->labels()->hasAny(['bug', 'hotfix'])) {
    // Priority handling
}

// Check if has all labels
if ($pr->labels()->hasAll(['approved', 'tested', 'documented'])) {
    $pr->merge();
}

Chaining Label Operations

PullRequests::find('owner/repo', 123)
    ->addLabel('ready-for-review')
    ->removeLabel('work-in-progress')
    ->requestReview('senior-dev')
    ->comment('Ready for review!');

Label-Based Automation

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

// Add labels based on file changes
$pr = PullRequests::find('owner/repo', 123);

$files = $pr->files()->get();

if ($files->wherePath('database/migrations/*')->isNotEmpty()) {
    $pr->addLabel('database');
}

if ($files->wherePath('tests/**/*')->isNotEmpty()) {
    $pr->addLabel('has-tests');
}

if ($files->wherePath('docs/**/*')->isNotEmpty()) {
    $pr->addLabel('documentation');
}

Priority Labeling

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

// Remove all priority labels and add new one
$pr->labels()
   ->removeMany(['low-priority', 'medium-priority', 'high-priority'])
   ->add('critical');

Repository Label Management

use ConduitUI\Pr\Services\RepositoryLabelManager;

$labels = new RepositoryLabelManager($github, 'owner/repo');

// Create new label
$labels->create(
    name: 'security',
    color: 'ff0000',
    description: 'Security-related changes'
);

// Update existing label
$labels->update(
    name: 'bug',
    color: 'd73a4a',
    description: 'Bug fix'
);

// Get all repository labels
$allLabels = $labels->get();

// Delete label
$labels->delete('wontfix');

Bulk Label Operations

// Add 'needs-review' to all open PRs
PullRequests::forRepo('owner/repo')
    ->whereOpen()
    ->get()
    ->each(fn($pr) => $pr->addLabel('needs-review'));

// Remove stale labels from closed PRs
PullRequests::forRepo('owner/repo')
    ->whereClosed()
    ->get()
    ->each(function($pr) {
        $pr->labels()
           ->removeMany(['needs-review', 'in-progress', 'waiting']);
    });

Label-Based Workflow

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

// Workflow state machine via labels
match(true) {
    $pr->labels()->has('approved') => 
        $pr->addLabel('ready-to-merge')->removeLabel('needs-review'),
    
    $pr->labels()->has('changes-requested') => 
        $pr->addLabel('in-progress')->removeLabel('needs-review'),
    
    $pr->labels()->has('ready-to-merge') && $pr->checksPass() => 
        $pr->merge(),
};

Size Labeling Based on Changes

$pr = PullRequests::find('owner/repo', 123);
$stats = $pr->files()->stats();

// Remove existing size labels
$pr->labels()->removeMany(['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL']);

// Add appropriate size label
$sizeLabel = match(true) {
    $stats->changes < 10 => 'size/XS',
    $stats->changes < 50 => 'size/S',
    $stats->changes < 200 => 'size/M',
    $stats->changes < 500 => 'size/L',
    default => 'size/XL',
};

$pr->addLabel($sizeLabel);

Testing Requirements

it('adds single label')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->addLabel('bug')
    )->toBeInstanceOf(PullRequestInstance::class);

it('adds multiple labels')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->addLabels(['bug', 'enhancement'])
    )->toBeInstanceOf(PullRequestInstance::class);

it('checks if label exists')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->labels()->has('bug')
    )->toBeBool();

it('replaces all labels')
    ->expect(fn() => 
        PullRequests::find('test/repo', 1)->setLabels(['new-label'])
    )->toBeInstanceOf(PullRequestInstance::class);

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

Dependencies

References

Labels

  • enhancement
  • labels
  • automation

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