Skip to content

Implement Label Management API with Fluent Builder #23

@jordanpartridge

Description

@jordanpartridge

Summary

Implement a comprehensive label management system with support for creating, updating, deleting, and querying labels at both the repository and issue level. Should provide a fluent API for batch operations and color management.

Requirements

Label DTO

File: src/Data/Label.php

namespace ConduitUI\Issue\Data;

readonly class Label
{
    public function __construct(
        public int $id,
        public string $name,
        public string $color, // hex color without #
        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'] ?? null,
            default: $data['default'] ?? false,
        );
    }
    
    /**
     * Get full hex color with #
     */
    public function hexColor(): string
    {
        return '#' . $this->color;
    }
    
    /**
     * Check if color is light or dark
     */
    public function isLightColor(): bool
    {
        $r = hexdec(substr($this->color, 0, 2));
        $g = hexdec(substr($this->color, 2, 2));
        $b = hexdec(substr($this->color, 4, 2));
        
        $brightness = (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
        
        return $brightness > 155;
    }
}

Repository-Level Label Manager

File: src/Services/RepositoryLabelManager.php

namespace ConduitUI\Issue\Services;

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

final class RepositoryLabelManager
{
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
    ) {}
    
    /**
     * Get all repository labels
     */
    public function all(): Collection
    {
        $response = $this->github->get(
            "/repos/{$this->fullName}/labels"
        );
        
        return collect($response->json())
            ->map(fn($label) => Label::fromArray($label));
    }
    
    /**
     * Get a specific label
     */
    public function find(string $name): Label
    {
        $response = $this->github->get(
            "/repos/{$this->fullName}/labels/{$name}"
        );
        
        return Label::fromArray($response->json());
    }
    
    /**
     * Create a new label
     */
    public function create(string $name, string $color, ?string $description = null): Label
    {
        $response = $this->github->post(
            "/repos/{$this->fullName}/labels",
            [
                'name' => $name,
                'color' => ltrim($color, '#'),
                'description' => $description,
            ]
        );
        
        return Label::fromArray($response->json());
    }
    
    /**
     * Update an existing label
     */
    public function update(string $name, array $attributes): Label
    {
        if (isset($attributes['color'])) {
            $attributes['color'] = ltrim($attributes['color'], '#');
        }
        
        $response = $this->github->patch(
            "/repos/{$this->fullName}/labels/{$name}",
            $attributes
        );
        
        return Label::fromArray($response->json());
    }
    
    /**
     * Delete a label
     */
    public function delete(string $name): bool
    {
        $response = $this->github->delete(
            "/repos/{$this->fullName}/labels/{$name}"
        );
        
        return $response->successful();
    }
    
    /**
     * Get a label builder for fluent creation
     */
    public function builder(): LabelBuilder
    {
        return new LabelBuilder($this->github, $this->fullName);
    }
    
    /**
     * Sync labels from an array
     */
    public function sync(array $labels): Collection
    {
        $existing = $this->all()->pluck('name')->toArray();
        $desired = collect($labels)->pluck('name')->toArray();
        
        // Delete labels not in desired list
        $toDelete = array_diff($existing, $desired);
        foreach ($toDelete as $name) {
            $this->delete($name);
        }
        
        // Create or update labels
        $results = collect();
        foreach ($labels as $label) {
            if (in_array($label['name'], $existing)) {
                $results->push($this->update($label['name'], $label));
            } else {
                $results->push($this->create(
                    $label['name'],
                    $label['color'],
                    $label['description'] ?? null
                ));
            }
        }
        
        return $results;
    }
}

Label Builder

File: src/Services/LabelBuilder.php

namespace ConduitUI\Issue\Services;

use ConduitUI\Connector\GitHub;
use ConduitUI\Issue\Data\Label;

final class LabelBuilder
{
    protected ?string $name = null;
    protected ?string $color = null;
    protected ?string $description = null;
    
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
    ) {}
    
    /**
     * Set label name
     */
    public function name(string $name): self
    {
        $this->name = $name;
        return $this;
    }
    
    /**
     * Set label color
     */
    public function color(string $color): self
    {
        $this->color = ltrim($color, '#');
        return $this;
    }
    
    /**
     * Set label description
     */
    public function description(string $description): self
    {
        $this->description = $description;
        return $this;
    }
    
    /**
     * Use a predefined color
     */
    public function red(): self
    {
        return $this->color('d73a4a');
    }
    
    public function orange(): self
    {
        return $this->color('d4a72c');
    }
    
    public function yellow(): self
    {
        return $this->color('fef2c0');
    }
    
    public function green(): self
    {
        return $this->color('0e8a16');
    }
    
    public function blue(): self
    {
        return $this->color('1d76db');
    }
    
    public function purple(): self
    {
        return $this->color('5319e7');
    }
    
    public function pink(): self
    {
        return $this->color('e99695');
    }
    
    public function gray(): self
    {
        return $this->color('d1d5da');
    }
    
    /**
     * Create the label
     */
    public function create(): Label
    {
        $response = $this->github->post(
            "/repos/{$this->fullName}/labels",
            [
                'name' => $this->name,
                'color' => $this->color,
                'description' => $this->description,
            ]
        );
        
        return Label::fromArray($response->json());
    }
}

Issue-Level Label Manager

File: src/Services/IssueLabelManager.php

namespace ConduitUI\Issue\Services;

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

final class IssueLabelManager
{
    public function __construct(
        protected GitHub $github,
        protected string $fullName,
        protected int $issueNumber,
    ) {}
    
    /**
     * Get all labels for this issue
     */
    public function all(): Collection
    {
        $response = $this->github->get(
            "/repos/{$this->fullName}/issues/{$this->issueNumber}/labels"
        );
        
        return collect($response->json())
            ->map(fn($label) => Label::fromArray($label));
    }
    
    /**
     * Add labels to issue
     */
    public function add(string|array $labels): Collection
    {
        $labels = is_array($labels) ? $labels : [$labels];
        
        $response = $this->github->post(
            "/repos/{$this->fullName}/issues/{$this->issueNumber}/labels",
            ['labels' => $labels]
        );
        
        return collect($response->json())
            ->map(fn($label) => Label::fromArray($label));
    }
    
    /**
     * Remove a label from issue
     */
    public function remove(string $label): bool
    {
        $response = $this->github->delete(
            "/repos/{$this->fullName}/issues/{$this->issueNumber}/labels/{$label}"
        );
        
        return $response->successful();
    }
    
    /**
     * Replace all labels
     */
    public function set(array $labels): Collection
    {
        $response = $this->github->put(
            "/repos/{$this->fullName}/issues/{$this->issueNumber}/labels",
            ['labels' => $labels]
        );
        
        return collect($response->json())
            ->map(fn($label) => Label::fromArray($label));
    }
    
    /**
     * Remove all labels
     */
    public function clear(): bool
    {
        $response = $this->github->delete(
            "/repos/{$this->fullName}/issues/{$this->issueNumber}/labels"
        );
        
        return $response->successful();
    }
}

Integration with Issues Facade

Add to: src/Facades/Issues.php

/**
 * @method static RepositoryLabelManager labels(string $fullName)
 */

Add to: src/Services/Issues.php

public function labels(string $fullName): RepositoryLabelManager
{
    return new RepositoryLabelManager($this->github, $fullName);
}

Usage Examples

use ConduitUI\Issue\Facades\Issues;

// Repository-level label management
Issues::labels('owner/repo')->all();

Issues::labels('owner/repo')
    ->create('bug', 'd73a4a', 'Something is broken');

Issues::labels('owner/repo')
    ->update('bug', [
        'color' => 'ff0000',
        'description' => 'Critical bug',
    ]);

Issues::labels('owner/repo')
    ->delete('old-label');

// Fluent label creation
Issues::labels('owner/repo')
    ->builder()
    ->name('priority-high')
    ->red()
    ->description('High priority issue')
    ->create();

Issues::labels('owner/repo')
    ->builder()
    ->name('enhancement')
    ->blue()
    ->description('New feature or enhancement')
    ->create();

// Sync labels from config
Issues::labels('owner/repo')->sync([
    ['name' => 'bug', 'color' => 'd73a4a', 'description' => 'Bug'],
    ['name' => 'enhancement', 'color' => '1d76db', 'description' => 'Enhancement'],
    ['name' => 'documentation', 'color' => '0e8a16', 'description' => 'Docs'],
]);

// Issue-level label management
Issues::find('owner/repo', 123)
    ->labels()
    ->add('bug');

Issues::find('owner/repo', 123)
    ->labels()
    ->add(['bug', 'priority-high']);

Issues::find('owner/repo', 123)
    ->labels()
    ->remove('wontfix');

Issues::find('owner/repo', 123)
    ->labels()
    ->set(['bug', 'verified']);

Issues::find('owner/repo', 123)
    ->labels()
    ->clear();

Acceptance Criteria

  • Label DTO with color utilities
  • RepositoryLabelManager for repo-level operations
  • IssueLabelManager for issue-level operations
  • LabelBuilder with fluent color methods
  • Predefined color constants (GitHub standard colors)
  • Label sync functionality
  • Batch add/remove operations
  • Full test coverage
  • Integration with IssueInstance
  • Follows established patterns

Dependencies

Technical Notes

GitHub Standard Label Colors:

  • Red (d73a4a) - Bugs, critical issues
  • Orange (d4a72c) - Warnings, deprecations
  • Yellow (fef2c0) - Needs attention
  • Green (0e8a16) - Improvements, enhancements
  • Blue (1d76db) - Information, documentation
  • Purple (5319e7) - Questions, discussions
  • Pink (e99695) - Design, UX
  • Gray (d1d5da) - Stale, wontfix

Color Format:
Always store colors without the # prefix to match GitHub's API format.

Label Naming:
GitHub label names are case-insensitive but preserve the original case.

API Reference:
https://docs.github.com/en/rest/issues/labels

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