feat: Implement Transfer Rules Engine for Cross-Repository Issue Routing#22
feat: Implement Transfer Rules Engine for Cross-Repository Issue Routing#22
Conversation
…atching and GraphQL API integration.
… GraphQL client validation, which is then used by the action executor.
There was a problem hiding this comment.
Pull request overview
This pull request implements a comprehensive Transfer Rules Engine for automated cross-repository issue routing, addressing issue #15. The implementation adds rule-based matching with configurable conditions (labels, content patterns, authors), GraphQL-based issue transfer functionality, and pipeline integration to prevent duplicate detection for transferred issues.
Changes:
- New
internal/transfer/package with rule matching engine supporting priority-based evaluation and case-insensitive matching with comprehensive test coverage (12 tests) - GraphQL client implementation for GitHub's
transferIssuemutation with proper node ID resolution - Pipeline reordering to execute transfer checks before similarity search, with metadata-based skip logic
- Configuration types for transfer rules with support for AND/OR logic across multiple condition types
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/transfer/types.go | Defines data structures for issue input and match results |
| internal/transfer/matcher.go | Implements rule matching engine with priority-based evaluation and case-insensitive matching logic |
| internal/transfer/matcher_test.go | Comprehensive test coverage for all matching scenarios (12 test cases) |
| internal/steps/transfer_check.go | Pipeline step that evaluates transfer rules and sets transfer target with skip metadata |
| internal/steps/similarity.go | Updated to skip duplicate detection when transfer is detected |
| internal/steps/action_executor.go | Updated to handle new TransferIssue signature returning URL and set Transferred flag |
| internal/integrations/github/graphql.go | New GraphQL client with transferIssue mutation and node ID resolution methods |
| internal/integrations/github/client.go | TransferIssue implementation using GraphQL API with validation and error handling |
| internal/integrations/github/auth.go | GraphQL client initialization for authenticated operations |
| internal/integrations/github/client_test.go | Updated tests for new TransferIssue signature |
| internal/core/pipeline/registry.go | Reordered pipeline to execute transfer_check before similarity_search |
| internal/core/config/config.go | Added TransferRule and TransferConfig types with merge logic |
| .claude/sessions/2026-02-04-0000.md | Development session tracking metadata |
| .claude/sessions/.current-session | Current session reference |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Validate input format | ||
| parts := strings.Split(targetRepo, "/") | ||
| if len(parts) != 2 { | ||
| return fmt.Errorf("invalid targetRepo format: expected 'owner/repo', got '%s'", targetRepo) | ||
| return "", fmt.Errorf("invalid targetRepo format: expected 'owner/repo', got '%s'", targetRepo) |
There was a problem hiding this comment.
The validation doesn't trim whitespace from the target repository string before parsing. A target like "owner/repo " (with trailing space) would pass validation but might cause issues with the GraphQL API. Consider trimming whitespace from the targetRepo parameter before validation and parsing to handle user input more robustly.
| if resp.StatusCode != http.StatusOK { | ||
| return nil, fmt.Errorf("GraphQL request failed with status %d: %s", resp.StatusCode, string(respBody)) |
There was a problem hiding this comment.
The error message includes the full response body which could potentially leak sensitive information in logs. Consider truncating the response body or only including a sanitized portion of it in the error message to avoid exposing sensitive data that might be in error responses.
| if err := json.Unmarshal(data, &result); err != nil { | ||
| return "", fmt.Errorf("failed to parse transfer result: %w", err) | ||
| } | ||
|
|
There was a problem hiding this comment.
The function returns the URL without validating that the transfer was successful. Consider adding a check to ensure the returned URL is not empty, similar to the validation done in GetIssueNodeID and GetRepositoryNodeID. An empty URL could indicate a failed transfer that wasn't caught by error handling.
| if result.TransferIssue.Issue.URL == "" { | |
| return "", fmt.Errorf("issue transfer failed: empty URL returned") | |
| } |
| // Transfer: child overrides if enabled or has rules | ||
| if child.Transfer.Enabled || len(child.Transfer.Rules) > 0 { | ||
| result.Transfer = child.Transfer |
There was a problem hiding this comment.
The merge logic for Transfer config only overrides if child.Transfer.Enabled is true or has rules. This means a child config cannot explicitly disable transfer if the parent has it enabled. Consider always merging the Transfer config if any field is set in the child, or add explicit handling for the case where a child wants to disable an enabled parent transfer configuration. For consistency with other boolean fields like CrossRepoSearch (line 232), consider whether Transfer.Enabled should be merged regardless of its value.
| // Transfer: child overrides if enabled or has rules | |
| if child.Transfer.Enabled || len(child.Transfer.Rules) > 0 { | |
| result.Transfer = child.Transfer | |
| // Transfer.Enabled: always take the child value so it can override parent true -> false and vice versa | |
| result.Transfer.Enabled = child.Transfer.Enabled | |
| // Transfer.Rules: child overrides rules if non-empty; otherwise inherit from parent | |
| if len(child.Transfer.Rules) > 0 { | |
| result.Transfer.Rules = child.Transfer.Rules |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Add whitespace trimming for targetRepo input validation - Truncate error response bodies to prevent sensitive data leaks - Add empty URL validation after transfer completion - Fix Transfer config merge logic to allow child to disable parent
Summary
Implements the Transfer Rules Engine for cross-repository issue routing as specified in issue #15. This enables automated issue transfers based on configurable rules matching labels, title/body content, and authors.
Changes
Core Transfer Logic
internal/transfer/with rule matching engineRuleMatcherevaluates rules by priority (descending order)Configuration
TransferRuleandTransferConfigtypes tointernal/core/config/config.goPipeline Integration
transfer_checkstep beforesimilarity_searchin pipeline (prevents duplicate detection for transfers)transfer_checksetsskip_duplicate_detectionmetadata when transfer is detectedsimilarity_searchto respect skip flagGitHub GraphQL Integration
internal/integrations/github/graphql.go- GraphQL client implementationtransferIssuemutation with proper node ID resolutionClient.TransferIssue()to use GraphQL API (returns new issue URL)auth.go)Action Executor
TransferIssuesignature returning(string, error)Result.Transferred = trueon successArchitecture
Configuration Example
Testing
Notes
Closes #15