From 1d425b2ec43a32febb6a591e5b3ca0ca17d85a11 Mon Sep 17 00:00:00 2001 From: Matt Glaman Date: Wed, 21 Oct 2026 16:35:17 -0500 Subject: [PATCH 1/2] Apply type and category filters to GitLab work item issue lists project:issues dropped the type argument and --category option when a project had migrated to GitLab work items, so every call returned all open issues. Map them to the state:: and category:: labels used on git.drupalcode.org and pass them as the GitLab labels filter. Fixes #391 Co-Authored-By: Claude Opus 5.5 --- .../Action/GitLab/ListGitLabIssuesAction.php | 16 ++++- src/Api/Enum/ProjectIssueCategory.php | 5 ++ src/Api/Enum/ProjectIssueType.php | 9 +++ src/Cli/Command/Project/ProjectIssues.php | 14 ++-- .../GitLab/ListGitLabIssuesActionTest.php | 66 +++++++++++++++++++ 5 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 tests/src/Action/GitLab/ListGitLabIssuesActionTest.php diff --git a/src/Api/Action/GitLab/ListGitLabIssuesAction.php b/src/Api/Action/GitLab/ListGitLabIssuesAction.php index 3b82e7fb..8d7b2032 100644 --- a/src/Api/Action/GitLab/ListGitLabIssuesAction.php +++ b/src/Api/Action/GitLab/ListGitLabIssuesAction.php @@ -4,6 +4,8 @@ namespace mglaman\DrupalOrg\Action\GitLab; +use mglaman\DrupalOrg\Enum\ProjectIssueCategory; +use mglaman\DrupalOrg\Enum\ProjectIssueType; use mglaman\DrupalOrg\GitLab\Client as GitLabClient; use mglaman\DrupalOrg\GitLab\Entity\GitLabIssue; use mglaman\DrupalOrg\Result\GitLab\GitLabIssuesResult; @@ -14,8 +16,13 @@ public function __construct(private readonly GitLabClient $gitLabClient) { } - public function __invoke(string $projectMachineName, string $state = 'opened', int $limit = 25): GitLabIssuesResult - { + public function __invoke( + string $projectMachineName, + string $state = 'opened', + int $limit = 25, + ProjectIssueType $type = ProjectIssueType::All, + ?ProjectIssueCategory $category = null, + ): GitLabIssuesResult { $params = [ 'state' => $state, 'per_page' => $limit, @@ -23,6 +30,11 @@ public function __invoke(string $projectMachineName, string $state = 'opened', i 'sort' => 'desc', ]; + $labels = array_filter([$type->gitLabLabel(), $category?->gitLabLabel()]); + if ($labels !== []) { + $params['labels'] = implode(',', $labels); + } + $data = $this->gitLabClient->getIssues('project/' . $projectMachineName, $params); $issues = array_map( static fn(\stdClass $item) => GitLabIssue::fromStdClass($item), diff --git a/src/Api/Enum/ProjectIssueCategory.php b/src/Api/Enum/ProjectIssueCategory.php index 57172f86..aeadf0c6 100644 --- a/src/Api/Enum/ProjectIssueCategory.php +++ b/src/Api/Enum/ProjectIssueCategory.php @@ -20,4 +20,9 @@ public function categoryId(): int ProjectIssueCategory::Plan => 5, }; } + + public function gitLabLabel(): string + { + return 'category::' . $this->value; + } } diff --git a/src/Api/Enum/ProjectIssueType.php b/src/Api/Enum/ProjectIssueType.php index c5b29c0c..15ea81dc 100644 --- a/src/Api/Enum/ProjectIssueType.php +++ b/src/Api/Enum/ProjectIssueType.php @@ -7,4 +7,13 @@ enum ProjectIssueType: string case All = 'all'; case Rtbc = 'rtbc'; case Review = 'review'; + + public function gitLabLabel(): ?string + { + return match ($this) { + ProjectIssueType::All => null, + ProjectIssueType::Rtbc => 'state::rtbc', + ProjectIssueType::Review => 'state::needsReview', + }; + } } diff --git a/src/Cli/Command/Project/ProjectIssues.php b/src/Cli/Command/Project/ProjectIssues.php index ccaca0f0..11ee64fa 100644 --- a/src/Cli/Command/Project/ProjectIssues.php +++ b/src/Cli/Command/Project/ProjectIssues.php @@ -70,10 +70,18 @@ protected function execute( ): int { $format = (string) $this->stdIn->getOption('format'); $limit = (int) $this->stdIn->getOption('limit'); + $type = ProjectIssueType::from((string) $this->stdIn->getArgument('type')); + $categoryOption = $this->stdIn->getOption('category'); + $category = $categoryOption !== null ? ProjectIssueCategory::from((string) $categoryOption) : null; if (!$this->projectData->hasIssueQueue) { $this->stdErr->writeln('Project uses GitLab work items (no Drupal.org issue queue).'); - $result = (new ListGitLabIssuesAction(new GitLabClient()))($this->projectData->machineName, limit: $limit); + $result = (new ListGitLabIssuesAction(new GitLabClient()))( + $this->projectData->machineName, + limit: $limit, + type: $type, + category: $category, + ); if ($this->writeFormatted($result, $format)) { return 0; } @@ -98,11 +106,9 @@ protected function execute( } $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')), + $type, (string) $this->stdIn->getOption('core'), $limit, $category diff --git a/tests/src/Action/GitLab/ListGitLabIssuesActionTest.php b/tests/src/Action/GitLab/ListGitLabIssuesActionTest.php new file mode 100644 index 00000000..8b84e8dc --- /dev/null +++ b/tests/src/Action/GitLab/ListGitLabIssuesActionTest.php @@ -0,0 +1,66 @@ + + */ + public static function filterProvider(): iterable + { + yield 'no filters' => [ProjectIssueType::All, null, null]; + yield 'rtbc' => [ProjectIssueType::Rtbc, null, 'state::rtbc']; + yield 'review' => [ProjectIssueType::Review, null, 'state::needsReview']; + yield 'category only' => [ProjectIssueType::All, ProjectIssueCategory::Bug, 'category::bug']; + yield 'type and category' => [ProjectIssueType::Rtbc, ProjectIssueCategory::Task, 'state::rtbc,category::task']; + } + + #[DataProvider('filterProvider')] + public function testFiltersMapToGitLabLabels( + ProjectIssueType $type, + ?ProjectIssueCategory $category, + ?string $expectedLabels, + ): void { + $expectedParams = [ + 'state' => 'opened', + 'per_page' => 10, + 'order_by' => 'created_at', + 'sort' => 'desc', + ]; + if ($expectedLabels !== null) { + $expectedParams['labels'] = $expectedLabels; + } + + $gitLabClient = $this->createMock(GitLabClient::class); + $gitLabClient->expects(self::once()) + ->method('getIssues') + ->with('project/smart_content', $expectedParams) + ->willReturn([]); + + $result = (new ListGitLabIssuesAction($gitLabClient))( + 'smart_content', + limit: 10, + type: $type, + category: $category, + ); + + self::assertSame('smart_content', $result->projectMachineName); + self::assertSame([], $result->issues); + } +} From 0aba87cae4ed125e56efaf2133dc6034555ea19e Mon Sep 17 00:00:00 2001 From: Matt Glaman Date: Wed, 21 Oct 2026 16:37:05 -0500 Subject: [PATCH 2/2] Expose type and category filters on gitlab_project_issues MCP tool Co-Authored-By: Claude Opus 5.5 --- src/Api/Mcp/ToolRegistry.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Api/Mcp/ToolRegistry.php b/src/Api/Mcp/ToolRegistry.php index cdd56950..7abe867b 100644 --- a/src/Api/Mcp/ToolRegistry.php +++ b/src/Api/Mcp/ToolRegistry.php @@ -222,8 +222,13 @@ public function gitlabProjectIssues( #[Schema(description: 'Issue state filter.', enum: ['opened', 'closed', 'all'])] string $state = 'opened', #[Schema(description: 'Maximum number of issues to return.', minimum: 1, maximum: 100)] - int $limit = 25 + int $limit = 25, + #[Schema(description: 'Filter issues by status type.', enum: ['all', 'rtbc', 'review'])] + string $type = 'all', + #[Schema(description: 'Filter issues by category.', enum: ['bug', 'task', 'feature', 'support', 'plan'])] + ?string $category = null ): mixed { - return (new ListGitLabIssuesAction(new GitLabClient()))($machineName, $state, $limit)->jsonSerialize(); + $issueCategory = $category !== null ? ProjectIssueCategory::from($category) : null; + return (new ListGitLabIssuesAction(new GitLabClient()))($machineName, $state, $limit, ProjectIssueType::from($type), $issueCategory)->jsonSerialize(); } }