Skip to content

Labels: Fluent label management API #30

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions