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
Summary
Implement expressive API for managing PR labels including add, remove, replace, and label-based automation.
Acceptance Criteria
Label Manager Interface
Label Manager Implementation
Repository Label Manager
Label DTO
Integration with PullRequestInstance
Usage Examples
Basic Label Operations
Checking Labels
Chaining Label Operations
Label-Based Automation
Priority Labeling
Repository Label Management
Bulk Label Operations
Label-Based Workflow
Size Labeling Based on Changes
Testing Requirements
Dependencies
References
Labels