diff --git a/skills/drupalorg-cli/SKILL.md b/skills/drupalorg-cli/SKILL.md index 3ea9291..107fbd4 100644 --- a/skills/drupalorg-cli/SKILL.md +++ b/skills/drupalorg-cli/SKILL.md @@ -115,8 +115,9 @@ drupalorg mr:logs 'project/drupal!708' ```bash # List open issues for a project -# type: all (default) or rtbc; --core defaults to 8.x; --limit defaults to 10 -drupalorg project:issues [project] [type] --format=llm +# type: all (default), rtbc, or review; --core defaults to 8.x; --limit defaults to 10 +# --category filters by issue type: bug, task, feature, support, plan (omit for all categories) +drupalorg project:issues [project] [type] [--category=bug|task|feature|support|plan] --format=llm # Search issues for a project by title keyword # project is optional; auto-detected from git remote if omitted diff --git a/src/Api/Action/Project/GetProjectIssuesAction.php b/src/Api/Action/Project/GetProjectIssuesAction.php index 205f632..621070e 100644 --- a/src/Api/Action/Project/GetProjectIssuesAction.php +++ b/src/Api/Action/Project/GetProjectIssuesAction.php @@ -6,6 +6,7 @@ use mglaman\DrupalOrg\Client; use mglaman\DrupalOrg\Entity\IssueNode; use mglaman\DrupalOrg\Entity\Project; +use mglaman\DrupalOrg\Enum\ProjectIssueCategory; use mglaman\DrupalOrg\Enum\ProjectIssueType; use mglaman\DrupalOrg\Request; use mglaman\DrupalOrg\Result\Project\ProjectIssuesResult; @@ -16,7 +17,7 @@ public function __construct(private readonly Client $client) { } - public function __invoke(Project $project, ProjectIssueType $type, string $core, int $limit): ProjectIssuesResult + public function __invoke(Project $project, ProjectIssueType $type, string $core, int $limit, ?ProjectIssueCategory $category = null): ProjectIssuesResult { $rawReleases = $this->client->requestRaw(new Request('node.json', [ 'field_release_project' => $project->nid, @@ -49,6 +50,10 @@ public function __invoke(Project $project, ProjectIssueType $type, string $core, } } + if ($category !== null) { + $apiParams['field_issue_category'] = $category->categoryId(); + } + $rawIssues = $this->client->requestRaw(new Request('node.json', $apiParams)); $issueList = (array) ($rawIssues->list ?? []); $issues = array_map( diff --git a/src/Api/Enum/ProjectIssueCategory.php b/src/Api/Enum/ProjectIssueCategory.php new file mode 100644 index 0000000..57172f8 --- /dev/null +++ b/src/Api/Enum/ProjectIssueCategory.php @@ -0,0 +1,23 @@ + 1, + ProjectIssueCategory::Task => 2, + ProjectIssueCategory::Feature => 3, + ProjectIssueCategory::Support => 4, + ProjectIssueCategory::Plan => 5, + }; + } +} diff --git a/src/Api/Mcp/ToolRegistry.php b/src/Api/Mcp/ToolRegistry.php index 34a9b0f..e3905bd 100644 --- a/src/Api/Mcp/ToolRegistry.php +++ b/src/Api/Mcp/ToolRegistry.php @@ -23,6 +23,7 @@ use mglaman\DrupalOrg\Client; use mglaman\DrupalOrg\Enum\MaintainerIssueType; use mglaman\DrupalOrg\Enum\MergeRequestState; +use mglaman\DrupalOrg\Enum\ProjectIssueCategory; use mglaman\DrupalOrg\Enum\ProjectIssueType; use mglaman\DrupalOrg\GitLab\Client as GitLabClient; @@ -85,13 +86,16 @@ public function projectGetIssues( #[Schema(description: "Core compatibility branch to filter by (e.g. '10.x', '11.x').")] string $core = '10.x', #[Schema(description: 'Maximum number of issues to return.', minimum: 1, maximum: 100)] - int $limit = 50 + int $limit = 50, + #[Schema(description: 'Filter issues by category.', enum: ['bug', 'task', 'feature', 'support', 'plan'])] + ?string $category = null ): mixed { $project = $this->client->getProject($machineName); if ($project === null) { throw new \RuntimeException("Project '$machineName' not found."); } - return (new GetProjectIssuesAction($this->client))($project, ProjectIssueType::from($type), $core, $limit)->jsonSerialize(); + $issueCategory = $category !== null ? ProjectIssueCategory::from($category) : null; + return (new GetProjectIssuesAction($this->client))($project, ProjectIssueType::from($type), $core, $limit, $issueCategory)->jsonSerialize(); } #[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.')] diff --git a/src/Cli/Command/Project/ProjectIssues.php b/src/Cli/Command/Project/ProjectIssues.php index 9f1591f..c3c54aa 100644 --- a/src/Cli/Command/Project/ProjectIssues.php +++ b/src/Cli/Command/Project/ProjectIssues.php @@ -3,6 +3,7 @@ namespace mglaman\DrupalOrgCli\Command\Project; use mglaman\DrupalOrg\Action\Project\GetProjectIssuesAction; +use mglaman\DrupalOrg\Enum\ProjectIssueCategory; use mglaman\DrupalOrg\Enum\ProjectIssueType; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; @@ -40,6 +41,13 @@ protected function configure(): void 'Limit', '10' ) + ->addOption( + 'category', + null, + InputOption::VALUE_OPTIONAL, + 'Issue category: bug, task, feature, support, plan', + null + ) ->addOption( 'format', 'f', @@ -59,11 +67,14 @@ protected function execute( OutputInterface $output ): int { $action = new GetProjectIssuesAction($this->client); + $categoryOption = $this->stdIn->getOption('category'); + $category = $categoryOption !== null ? ProjectIssueCategory::from((string) $categoryOption) : null; $result = $action( $this->projectData, ProjectIssueType::from((string) $this->stdIn->getArgument('type')), (string) $this->stdIn->getOption('core'), - (int) $this->stdIn->getOption('limit') + (int) $this->stdIn->getOption('limit'), + $category ); if ($this->writeFormatted($result, (string) $this->stdIn->getOption('format'))) { diff --git a/tests/src/Action/Project/GetProjectIssuesActionTest.php b/tests/src/Action/Project/GetProjectIssuesActionTest.php index 4214d92..2790e25 100644 --- a/tests/src/Action/Project/GetProjectIssuesActionTest.php +++ b/tests/src/Action/Project/GetProjectIssuesActionTest.php @@ -6,8 +6,10 @@ use mglaman\DrupalOrg\Client; use mglaman\DrupalOrg\Entity\IssueNode; use mglaman\DrupalOrg\Entity\Project; +use mglaman\DrupalOrg\Enum\ProjectIssueCategory; use mglaman\DrupalOrg\Enum\ProjectIssueType; use mglaman\DrupalOrg\Result\Project\ProjectIssuesResult; +use mglaman\DrupalOrg\Request; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -44,6 +46,46 @@ private static function makeRawIssues(): \stdClass ]; } + public function testInvokeWithCategory(): void + { + $project = Project::fromStdClass(self::projectFixture()); + + $client = $this->createMock(Client::class); + $capturedParams = []; + $client->method('requestRaw')->willReturnCallback( + function (Request $request) use (&$capturedParams) { + $capturedParams[] = $request->getOptions(); + return (object) ['list' => []]; + } + ); + + $action = new GetProjectIssuesAction($client); + $action($project, ProjectIssueType::All, '8.x', 10, ProjectIssueCategory::Bug); + + // Second call is the issues request — it should include field_issue_category = 1 (Bug) + self::assertArrayHasKey('field_issue_category', $capturedParams[1]); + self::assertSame(1, $capturedParams[1]['field_issue_category']); + } + + public function testInvokeWithoutCategoryDoesNotAddParam(): void + { + $project = Project::fromStdClass(self::projectFixture()); + + $client = $this->createMock(Client::class); + $capturedParams = []; + $client->method('requestRaw')->willReturnCallback( + function (Request $request) use (&$capturedParams) { + $capturedParams[] = $request->getOptions(); + return (object) ['list' => []]; + } + ); + + $action = new GetProjectIssuesAction($client); + $action($project, ProjectIssueType::All, '8.x', 10); + + self::assertArrayNotHasKey('field_issue_category', $capturedParams[1]); + } + public function testInvoke(): void { $project = Project::fromStdClass(self::projectFixture());