-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
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
- Requires:
ConduitUI\Connector\GitHub - Related to: Add Active Record entity layer (matching conduit-ui/pr pattern) #2 (IssueInstance integration)
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
coderabbitai
Metadata
Metadata
Assignees
Labels
No labels