Skip to content

Commit c77961c

Browse files
Copilotmglaman
andauthored
Add --category filter to project:issues for filtering by issue type (#328)
* Initial plan * feat: add --category option to project:issues for filtering by issue type Co-authored-by: mglaman <3698644+mglaman@users.noreply.github.com> Agent-Logs-Url: https://github.com/mglaman/drupalorg-cli/sessions/ac0e1342-727c-4ba5-8482-d218b7aa6098 * docs: update drupalorg-cli skill to document --category option on project:issues Co-authored-by: mglaman <3698644+mglaman@users.noreply.github.com> Agent-Logs-Url: https://github.com/mglaman/drupalorg-cli/sessions/f99f29f0-fa50-44ce-b91a-3c0c23eb3f42 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mglaman <3698644+mglaman@users.noreply.github.com>
1 parent e655423 commit c77961c

6 files changed

Lines changed: 92 additions & 6 deletions

File tree

skills/drupalorg-cli/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ drupalorg mr:logs 'project/drupal!708'
115115

116116
```bash
117117
# List open issues for a project
118-
# type: all (default) or rtbc; --core defaults to 8.x; --limit defaults to 10
119-
drupalorg project:issues [project] [type] --format=llm
118+
# type: all (default), rtbc, or review; --core defaults to 8.x; --limit defaults to 10
119+
# --category filters by issue type: bug, task, feature, support, plan (omit for all categories)
120+
drupalorg project:issues [project] [type] [--category=bug|task|feature|support|plan] --format=llm
120121

121122
# Search issues for a project by title keyword
122123
# project is optional; auto-detected from git remote if omitted

src/Api/Action/Project/GetProjectIssuesAction.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use mglaman\DrupalOrg\Client;
77
use mglaman\DrupalOrg\Entity\IssueNode;
88
use mglaman\DrupalOrg\Entity\Project;
9+
use mglaman\DrupalOrg\Enum\ProjectIssueCategory;
910
use mglaman\DrupalOrg\Enum\ProjectIssueType;
1011
use mglaman\DrupalOrg\Request;
1112
use mglaman\DrupalOrg\Result\Project\ProjectIssuesResult;
@@ -16,7 +17,7 @@ public function __construct(private readonly Client $client)
1617
{
1718
}
1819

19-
public function __invoke(Project $project, ProjectIssueType $type, string $core, int $limit): ProjectIssuesResult
20+
public function __invoke(Project $project, ProjectIssueType $type, string $core, int $limit, ?ProjectIssueCategory $category = null): ProjectIssuesResult
2021
{
2122
$rawReleases = $this->client->requestRaw(new Request('node.json', [
2223
'field_release_project' => $project->nid,
@@ -49,6 +50,10 @@ public function __invoke(Project $project, ProjectIssueType $type, string $core,
4950
}
5051
}
5152

53+
if ($category !== null) {
54+
$apiParams['field_issue_category'] = $category->categoryId();
55+
}
56+
5257
$rawIssues = $this->client->requestRaw(new Request('node.json', $apiParams));
5358
$issueList = (array) ($rawIssues->list ?? []);
5459
$issues = array_map(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace mglaman\DrupalOrg\Enum;
4+
5+
enum ProjectIssueCategory: string
6+
{
7+
case Bug = 'bug';
8+
case Task = 'task';
9+
case Feature = 'feature';
10+
case Support = 'support';
11+
case Plan = 'plan';
12+
13+
public function categoryId(): int
14+
{
15+
return match ($this) {
16+
ProjectIssueCategory::Bug => 1,
17+
ProjectIssueCategory::Task => 2,
18+
ProjectIssueCategory::Feature => 3,
19+
ProjectIssueCategory::Support => 4,
20+
ProjectIssueCategory::Plan => 5,
21+
};
22+
}
23+
}

src/Api/Mcp/ToolRegistry.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use mglaman\DrupalOrg\Client;
2424
use mglaman\DrupalOrg\Enum\MaintainerIssueType;
2525
use mglaman\DrupalOrg\Enum\MergeRequestState;
26+
use mglaman\DrupalOrg\Enum\ProjectIssueCategory;
2627
use mglaman\DrupalOrg\Enum\ProjectIssueType;
2728
use mglaman\DrupalOrg\GitLab\Client as GitLabClient;
2829

@@ -85,13 +86,16 @@ public function projectGetIssues(
8586
#[Schema(description: "Core compatibility branch to filter by (e.g. '10.x', '11.x').")]
8687
string $core = '10.x',
8788
#[Schema(description: 'Maximum number of issues to return.', minimum: 1, maximum: 100)]
88-
int $limit = 50
89+
int $limit = 50,
90+
#[Schema(description: 'Filter issues by category.', enum: ['bug', 'task', 'feature', 'support', 'plan'])]
91+
?string $category = null
8992
): mixed {
9093
$project = $this->client->getProject($machineName);
9194
if ($project === null) {
9295
throw new \RuntimeException("Project '$machineName' not found.");
9396
}
94-
return (new GetProjectIssuesAction($this->client))($project, ProjectIssueType::from($type), $core, $limit)->jsonSerialize();
97+
$issueCategory = $category !== null ? ProjectIssueCategory::from($category) : null;
98+
return (new GetProjectIssuesAction($this->client))($project, ProjectIssueType::from($type), $core, $limit, $issueCategory)->jsonSerialize();
9599
}
96100

97101
#[McpTool(annotations: new ToolAnnotations(readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true), name: 'issue_search', description: 'Search issues for a Drupal.org project by title keyword.')]

src/Cli/Command/Project/ProjectIssues.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace mglaman\DrupalOrgCli\Command\Project;
44

55
use mglaman\DrupalOrg\Action\Project\GetProjectIssuesAction;
6+
use mglaman\DrupalOrg\Enum\ProjectIssueCategory;
67
use mglaman\DrupalOrg\Enum\ProjectIssueType;
78
use Symfony\Component\Console\Helper\Table;
89
use Symfony\Component\Console\Helper\TableSeparator;
@@ -40,6 +41,13 @@ protected function configure(): void
4041
'Limit',
4142
'10'
4243
)
44+
->addOption(
45+
'category',
46+
null,
47+
InputOption::VALUE_OPTIONAL,
48+
'Issue category: bug, task, feature, support, plan',
49+
null
50+
)
4351
->addOption(
4452
'format',
4553
'f',
@@ -59,11 +67,14 @@ protected function execute(
5967
OutputInterface $output
6068
): int {
6169
$action = new GetProjectIssuesAction($this->client);
70+
$categoryOption = $this->stdIn->getOption('category');
71+
$category = $categoryOption !== null ? ProjectIssueCategory::from((string) $categoryOption) : null;
6272
$result = $action(
6373
$this->projectData,
6474
ProjectIssueType::from((string) $this->stdIn->getArgument('type')),
6575
(string) $this->stdIn->getOption('core'),
66-
(int) $this->stdIn->getOption('limit')
76+
(int) $this->stdIn->getOption('limit'),
77+
$category
6778
);
6879

6980
if ($this->writeFormatted($result, (string) $this->stdIn->getOption('format'))) {

tests/src/Action/Project/GetProjectIssuesActionTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
use mglaman\DrupalOrg\Client;
77
use mglaman\DrupalOrg\Entity\IssueNode;
88
use mglaman\DrupalOrg\Entity\Project;
9+
use mglaman\DrupalOrg\Enum\ProjectIssueCategory;
910
use mglaman\DrupalOrg\Enum\ProjectIssueType;
1011
use mglaman\DrupalOrg\Result\Project\ProjectIssuesResult;
12+
use mglaman\DrupalOrg\Request;
1113
use PHPUnit\Framework\Attributes\CoversClass;
1214
use PHPUnit\Framework\TestCase;
1315

@@ -44,6 +46,46 @@ private static function makeRawIssues(): \stdClass
4446
];
4547
}
4648

49+
public function testInvokeWithCategory(): void
50+
{
51+
$project = Project::fromStdClass(self::projectFixture());
52+
53+
$client = $this->createMock(Client::class);
54+
$capturedParams = [];
55+
$client->method('requestRaw')->willReturnCallback(
56+
function (Request $request) use (&$capturedParams) {
57+
$capturedParams[] = $request->getOptions();
58+
return (object) ['list' => []];
59+
}
60+
);
61+
62+
$action = new GetProjectIssuesAction($client);
63+
$action($project, ProjectIssueType::All, '8.x', 10, ProjectIssueCategory::Bug);
64+
65+
// Second call is the issues request — it should include field_issue_category = 1 (Bug)
66+
self::assertArrayHasKey('field_issue_category', $capturedParams[1]);
67+
self::assertSame(1, $capturedParams[1]['field_issue_category']);
68+
}
69+
70+
public function testInvokeWithoutCategoryDoesNotAddParam(): void
71+
{
72+
$project = Project::fromStdClass(self::projectFixture());
73+
74+
$client = $this->createMock(Client::class);
75+
$capturedParams = [];
76+
$client->method('requestRaw')->willReturnCallback(
77+
function (Request $request) use (&$capturedParams) {
78+
$capturedParams[] = $request->getOptions();
79+
return (object) ['list' => []];
80+
}
81+
);
82+
83+
$action = new GetProjectIssuesAction($client);
84+
$action($project, ProjectIssueType::All, '8.x', 10);
85+
86+
self::assertArrayNotHasKey('field_issue_category', $capturedParams[1]);
87+
}
88+
4789
public function testInvoke(): void
4890
{
4991
$project = Project::fromStdClass(self::projectFixture());

0 commit comments

Comments
 (0)