From c46bca8e17af58e3682f48480e67a51d277e2243 Mon Sep 17 00:00:00 2001 From: Riccardo Ravaioli Date: Mon, 17 Nov 2025 20:44:25 +0100 Subject: [PATCH] Claude Code command for automated bug labeling via MCP JIRA server This commit adds an AI-powered bug labeling system for the networking team: /label-bugs command: - Automatically analyzes and labels JIRA bugs using area_labels.csv mapping - Uses MCP JIRA server by default for efficient data retrieval - Supports --dry-run flag to preview labels without applying them - Supports --api flag to use Python API through network_bugs_overview script as fallback - Implements confidence-based labeling: high (>80%), medium (40-80%), low (<40%) - Auto-applies high-confidence labels, prompts user for medium/low confidence - Detects backports via issue dependencies and applies SDN:Backport label - Skips bugs that already have SDN area labels Additional changes: - Add comprehensive MCP JIRA server configuration instructions to README.md - Document /label-bugs usage and examples in README.md --- jira-scripts/.claude/commands/label-bugs.md | 177 ++++++++++++++++++++ jira-scripts/README.md | 72 ++++++++ jira-scripts/area_labels.csv | 44 +++++ jira-scripts/network_bugs_overview | 121 ++++++++++++- 4 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 jira-scripts/.claude/commands/label-bugs.md create mode 100644 jira-scripts/area_labels.csv diff --git a/jira-scripts/.claude/commands/label-bugs.md b/jira-scripts/.claude/commands/label-bugs.md new file mode 100644 index 0000000..2d69ed0 --- /dev/null +++ b/jira-scripts/.claude/commands/label-bugs.md @@ -0,0 +1,177 @@ +# Auto-Label JIRA Bugs + +You are helping automate bug labeling for the networking team. + +## Usage + +- `/label-bugs` - Analyze and apply labels to unlabeled bugs (uses MCP JIRA server) +- `/label-bugs --dry-run` - Analyze bugs and show what labels would be applied WITHOUT actually applying them +- `/label-bugs --api` - Use Python API instead of MCP JIRA server (fallback mode) +- `/label-bugs --api --dry-run` - Use Python API in dry-run mode + +## Mode Selection + +**Default (MCP Mode)**: Uses the MCP JIRA server tools (`mcp__jira-atlassian__*`) + +**API Mode** (`--api` flag): Falls back to Python script using direct JIRA API via `jira-scripts/network_bugs_overview` + +If the user specifies `--api`, you MUST use the Python script instead of MCP tools for both fetching bugs and applying labels. + +## Dry Run Mode + +If the user invokes this command with `--dry-run`, `dry-run`, or `-n`: +- Perform all analysis steps normally +- Show what label would be applied to each bug +- **DO NOT** apply any labels to JIRA +- **DO NOT** ask user questions (no AskUserQuestion tool calls) +- Just analyze and display results for all bugs, including medium and low confidence ones +- Clearly indicate in the output that this is a dry run +- Mark items with `[DRY RUN]` prefix in the summary + +## Your Task + +1. **Read the label mapping** from `jira-scripts/area_labels.csv`: + - First column: Feature/area description + - Second column: Label to apply + +2. **Fetch unlabeled bugs** from JIRA: + + **MCP Mode (default - when --api is NOT specified)**: + - Use the `mcp__jira-atlassian__jira_search` tool + - Use this JQL query: + ``` + project = OCPBUGS AND component in ("Networking / openshift-sdn", "Networking / ovn-kubernetes", "Networking / cloud-network-config-controller", "Networking / ingress-node-firewall", "Networking / cluster-network-operator", "Networking / network-tools") AND resolution = Unresolved AND (assignee = "bbennett@redhat.com" OR assignee is EMPTY) ORDER BY Rank DESC + ``` + - Request fields: `summary,description,labels,components,issuelinks` + + **API Mode (when --api is specified)**: + - Use the Bash tool to run: `cd jira-scripts && ./network_bugs_overview --query "YOUR_JQL_QUERY_HERE"` + - Use the same JQL query as above + - The script will output detailed bug information including ID, summary, description, labels, components, issue links, and comments + +3. **For each bug without an area label**: + - **In MCP mode**: Read additional details using `mcp__jira-atlassian__jira_get_issue` if needed + - **In API mode**: All details are already available from step 2 + - **Check if it's a backport**: If the bug has dependencies (depends on another bug), it's a backport → apply `SDN:Backport` label + - Otherwise, analyze the content and determine which ONE label from the CSV best fits + - **Confidence levels**: + - **High confidence** (>80% sure): Automatically apply the label + - **Medium confidence** (40-80% sure): Present top 2-3 candidates to user for selection + - **Low confidence** (<40% sure): Ask user to review and choose + +4. **For medium and low confidence bugs** (SKIP THIS STEP ENTIRELY in dry-run mode): + - **In normal mode**: After displaying the summary, use the AskUserQuestion tool to ask the user which label to apply for each bug. Present the top 2-3 alternatives as options. + - **In dry-run mode**: DO NOT use AskUserQuestion. Just display the analysis with proposed label and alternatives. + - **IMPORTANT**: Always include a "Skip/Defer" option that allows the user to skip applying any label to the bug + - **CRITICAL**: The "Skip/Defer" option MUST ALWAYS be option 1 (the first option) so users can quickly press 1 to skip bugs they want to defer + - **In the question text**: Include a brief summary of the bug description (2-3 sentences max) and a clickable link to the bug + - **Format**: "OCPBUGS-XXXXX: [Brief summary of the issue]. Link: https://issues.redhat.com/browse/OCPBUGS-XXXXX" + - Example question: "OCPBUGS-64932: Time drift during Chrony configuration causes ovnkube-controller certificate to become invalid, causing pods to crash. The certificate NotBefore time is ahead of worker node time. Link: https://issues.redhat.com/browse/OCPBUGS-64932 - Which label should be applied?" + +5. **Apply the labels** to JIRA (skip this step if in dry-run mode): + - **In MCP mode**: Use `mcp__jira-atlassian__jira_update_issue` to apply labels + - **In API mode**: Use the Bash tool to run: `cd jira-scripts && ./network_bugs_overview --label-bug OCPBUGS-XXXXX --label "SDN:LabelName"` + +6. **Display analysis results** using this standardized format for EACH bug: + + ``` + OCPBUGS-12345 - Bug title here (truncated to 80 chars if needed) + - Proposed label: SDN:OVNK:EgressIP + - Confidence: 95% + - Reasoning: Brief explanation of why this label was chosen + - URL: https://issues.redhat.com/browse/OCPBUGS-12345 + ``` + + For medium and low confidence bugs, also show: + ``` + - Top alternatives: SDN:Platform:OVNK, SDN:OVNK:AdminNetworkPolicy + - URL: https://issues.redhat.com/browse/OCPBUGS-12345 + ``` + +7. **Print final summary** at the end grouped by confidence level: + + Normal mode: + ``` + 🟢 High confidence (auto-applied): + OCPBUGS-12345 - Bug title here + - Proposed label: SDN:OVNK:EgressIP + - Confidence: 95% + - Reasoning: Explicitly about EgressIP failover issues with IPv6 + - URL: https://issues.redhat.com/browse/OCPBUGS-12345 + + OCPBUGS-12346 - Another bug title + - Proposed label: SDN:Platform:CNO + - Confidence: 85% + - Reasoning: CNO configuration and deployment issue + - URL: https://issues.redhat.com/browse/OCPBUGS-12346 + + 🟡 Medium confidence (review recommended): + OCPBUGS-12347 - Third bug + - Proposed label: SDN:OVNK:NetworkPolicy + - Confidence: 70% + - Reasoning: Network policy enforcement behavior + - Top alternatives: SDN:Platform:OVNK, SDN:OVNK:AdminNetworkPolicy + - URL: https://issues.redhat.com/browse/OCPBUGS-12347 + + 🔴 Low confidence (user input needed): + OCPBUGS-12349 - Fifth bug + - Proposed label: SDN:Tooling + - Confidence: 35% + - Reasoning: May be related to must-gather or debugging tools + - Top alternatives: SDN:Platform:CNO, SDN:Metrics + - URL: https://issues.redhat.com/browse/OCPBUGS-12349 + + User selected: + OCPBUGS-12350 - Sixth bug - SDN:OVNK:EgressIP (user confirmed) + - URL: https://issues.redhat.com/browse/OCPBUGS-12350 + + OCPBUGS-12351 - Seventh bug - Skipped (no label applied) + - URL: https://issues.redhat.com/browse/OCPBUGS-12351 + ``` + + Dry-run mode (same format with [DRY RUN] prefix, no user interaction): + ``` + 🟢 High confidence: + [DRY RUN] OCPBUGS-12345 - Bug title here + - Proposed label: SDN:OVNK:EgressIP + - Confidence: 95% + - Reasoning: Explicitly about EgressIP failover issues with IPv6 + - URL: https://issues.redhat.com/browse/OCPBUGS-12345 + + 🟡 Medium confidence: + [DRY RUN] OCPBUGS-12347 - Third bug + - Proposed label: SDN:OVNK:NetworkPolicy + - Confidence: 70% + - Reasoning: Network policy enforcement behavior + - Top alternatives: SDN:Platform:OVNK, SDN:OVNK:AdminNetworkPolicy + - URL: https://issues.redhat.com/browse/OCPBUGS-12347 + + 🔴 Low confidence: + [DRY RUN] OCPBUGS-12349 - Fifth bug + - Proposed label: SDN:Tooling + - Confidence: 35% + - Reasoning: May be related to must-gather or debugging tools + - Top alternatives: SDN:Platform:CNO, SDN:Metrics + - URL: https://issues.redhat.com/browse/OCPBUGS-12349 + ``` + +## Important Rules + +- **Use MCP JIRA server tools by default** (`mcp__jira-atlassian__*`). Only use Python scripts when `--api` flag is specified +- Each bug gets **exactly ONE** area label +- Backports always get `SDN:Backport` (check bug dependencies/links) +- If a bug already has an area label (any label starting with "SDN:" or "SDN-"), skip it +- **In dry-run mode**: NEVER use AskUserQuestion tool - just analyze and display all results +- **In normal mode**: Be conservative - when in doubt, ask the user rather than applying wrong label +- **Always provide a "Skip/Defer" option** when asking the user about medium/low confidence bugs (normal mode only) +- **The "Skip/Defer" option MUST be option 1 (first option) in every user question dialog** so users can quickly press 1 to skip +- **In AskUserQuestion dialogs**: Include a brief bug summary (2-3 sentences) and a link to the bug (https://issues.redhat.com/browse/OCPBUGS-XXXXX) +- Consider all available context: title, description, affected components, issue links +- Use the standardized output format for each bug (key, title, proposed label, confidence %, reasoning) +- **ALWAYS include the bug URL as the LAST item** for each bug in the format: `- URL: https://issues.redhat.com/browse/OCPBUGS-XXXXX` +- Group bugs by confidence level in the summary: high, medium, low, and user selected (in normal mode) +- In dry-run mode, group by: high, medium, and low confidence only (no "user selected" section) +- Show emoji once at the beginning of each confidence group: 🟢 🟡 🔴 +- For medium and low confidence bugs, show the top 2-3 alternative labels considered +- Skip groups that have no bugs (e.g., if no low confidence bugs, don't show that section) +- Track skipped bugs separately in the summary diff --git a/jira-scripts/README.md b/jira-scripts/README.md index ab6993f..2ff2ae4 100644 --- a/jira-scripts/README.md +++ b/jira-scripts/README.md @@ -1,6 +1,7 @@ # Quick reference - [Download source code and install dependencies](#download-source-code-and-install-dependencies) - [Configure jira client](#configure-jira-client) +- [Configure MCP JIRA Server for Claude Code](#configure-mcp-jira-server-for-claude-code) - [Run network_bugs_overview script](#run-network_bugs_overview-script) - [Documentation](#documentation) @@ -38,6 +39,77 @@ secrets = { } ``` +## Configure MCP JIRA Server for Claude Code + +If you're using [Claude Code](https://claude.ai/code), you can enable the MCP JIRA server to allow Claude to interact with JIRA directly without running Python scripts. This enables Claude to: +- Search for bugs using JQL queries +- Fetch bug details, comments, and metadata +- Create, update, and label issues +- Manage sprints, boards, and workflows + +### Setup Instructions + +1. **Generate a JIRA Personal Access Token** (same as above - go to https://issues.redhat.com → Profile → Personal Access Tokens → Create token) + +2. **Configure MCP Server in Claude Code**: + + Open your project in Claude Code, then run: + ```bash + claude mcp add + ``` + + Or manually edit `.claude/settings.json` in your project directory and add the following MCP server configuration: + + ```json + { + "mcpServers": { + "jira-atlassian": { + "type": "stdio", + "command": "podman", + "args": [ + "run", + "--rm", + "-i", + "-e", "JIRA_URL", + "-e", "JIRA_PERSONAL_TOKEN", + "-e", "JIRA_SSL_VERIFY", + "ghcr.io/sooperset/mcp-atlassian:latest" + ], + "env": { + "JIRA_URL": "https://issues.redhat.com", + "JIRA_PERSONAL_TOKEN": "YOUR_TOKEN_HERE", + "JIRA_SSL_VERIFY": "true" + } + } + } + } + ``` + +3. **Replace `YOUR_TOKEN_HERE`** with your actual JIRA personal access token + +4. **Restart Claude Code** to load the MCP server + +5. **Verify the setup** by running `/mcp` in Claude Code to see available JIRA tools + +### Usage with Claude Code + +Once configured, you can ask Claude to: +- "Find all unresolved bugs assigned to me in JIRA" +- "Search for bugs in OCPBUGS project related to networking" +- "Get details for OCPBUGS-12345" +- "Create a new bug in project OCPBUGS" +- "Update bug OCPBUGS-12345 with label SDN:Platform:OVNK" + +You can also use the built-in `/label-bugs` command to automatically analyze and label unlabeled networking bugs: +- `/label-bugs` - Analyze unlabeled bugs and apply appropriate area labels +- `/label-bugs --dry-run` - Preview what labels would be applied without making changes + +The `/label-bugs` command uses AI to analyze bug content and automatically assigns the appropriate SDN area labels based on the mapping in `area_labels.csv`. + +See the [MCP JIRA documentation](https://github.com/sooperset/mcp-atlassian) for all available tools and capabilities. + +**Important**: Keep your JIRA personal access token secret. Do not commit `.claude/settings.json` to version control if it contains your token. Consider adding it to `.gitignore`. + ## Run network_bugs_overview script The available input arguments are the following: diff --git a/jira-scripts/area_labels.csv b/jira-scripts/area_labels.csv new file mode 100644 index 0000000..e1c4811 --- /dev/null +++ b/jira-scripts/area_labels.csv @@ -0,0 +1,44 @@ +ALL Backports,SDN:Backport +Bug caused due to lack of test coverage?,SDN:LackingTestCoverage +All SDN Bugs (no need to split on Area),SDN:Platform:SDN +CNO,SDN:Platform:CNO +HyperShift Specific,SDN:Platform:HyperShift +MicroShift Specific,SDN:Platform:MicroShift +CNCC,SDN:Platform:CNCC +Ingress Node Firewall,SDN:Platform:IngressNodeFirewall +OVNK/OVN (anything that doesn't fit on the feature level),SDN:Platform:OVNK +"MCO, ConfigureOVS",SDN:Platform:OVS-Configure +SAST Scans,SDN-SAST-SCAN +Scale,SDN:Scale +"RBAC, NodeIdentity, Certificates",SDN:Security +CVE,SDN:CVE +CI,SDN:CI +"ART Bumps, Dockerfile changes, OVN Bump",SDN-ART-BUMP +"MustGather, NetworkTools",SDN:Tooling +Metrics,SDN:Metrics +SNO,SDN:Platform:SNO +Secondary Networks (NAD),SDN:OVNK:SecondaryNetworks +UDN Primary,SDN:OVNK:UserDefinedNetworks:Primary +UDN Secondary,SDN:OVNK:UserDefinedNetworks:Secondary +BGP Area,SDN:OVNK:BGP +QE can reproduce from customer bug,QE:Reproduced +EgressQoS,SDN:OVNK:EgressQoS +EgressIP,SDN:OVNK:EgressIP +EgressFirewall,SDN:OVNK:EgressFirewall +IPSEC,SDN:OVNK:IPSEC +APBRoute,SDN:OVNK:APBR +NetworkPolicy,SDN:OVNK:NetworkPolicy +AdminNetworkPolicy,SDN:OVNK:AdminNetworkPolicy +Multicast,SDN:OVNK:Multicast +HCP Live Migration,SDN:OVNK:KubevirtVMLiveMigration +CNI Live Migration,SDN:OVNK:CNILiveMigration +CNI Offline Migration,SDN:OVNK:CNIOfflineMigration +OVNK MTU Migration,SDN:OVNK:MTUMigration +EgressRouter,SDN:OVNK:EgressRouter +"Services, EndpointSlices",SDN:OVNK:SVC&EPS +"IPAM, AddLogicalPort, PodCNI",SDN:OVNK:Pods +"Anything in NodePkg, Gateway",SDN:OVNK:NodeController +Anything in DefaultNetController,SDN:OVNK:ZoneController +Anything in ClusterManager,SDN:OVNK:ClusterManager +old ICNI ones <=4.13,SDN:OVNK:ICNI +DPU,SDN:OVNK:DPU diff --git a/jira-scripts/network_bugs_overview b/jira-scripts/network_bugs_overview index 4ff375f..c84690c 100755 --- a/jira-scripts/network_bugs_overview +++ b/jira-scripts/network_bugs_overview @@ -1186,6 +1186,21 @@ def parse_input_args(): help="List bug issues assigned to 'sdn-team bot' that are older than 4 months", action="store_true", ) + parser.add_argument( + "--query", + type=str, + help="Execute a custom JQL query and print the bug IDs and summaries", + ) + parser.add_argument( + "--label-bug", + type=str, + help="Bug ID to apply a label to (e.g., OCPBUGS-123). Use with --label", + ) + parser.add_argument( + "--label", + type=str, + help="Label to apply to the bug specified with --label-bug", + ) args = parser.parse_args() @@ -1220,11 +1235,107 @@ def parse_input_args(): "process_github_issues": process_github_issues, "print_bug": args.print_bug, "recent_bot_bug_comments": bool(args.recent_bot_bug_comments), - "old_bot_bugs": bool(args.old_bot_bugs) + "old_bot_bugs": bool(args.old_bot_bugs), + "query": args.query, + "label_bug": args.label_bug, + "label": args.label, } return params +def run_custom_query(query_str): + """Execute a custom JQL query and print detailed bug information.""" + jira_client = init_jira() + print(f"Running JQL query: {query_str}") + print("=" * 80) + bugs = run_jira_query(jira_client, query_str) + + if not bugs: + print("No bugs found matching the query.") + return + + print(f"\nFound {len(bugs)} bugs:\n") + for bug in bugs: + bug_id = bug.key + bug_url = get_jira_issue_url(bug_id) + summary = bug.get_field("summary") + description = bug.get_field("description") or "" + labels = bug.fields.labels or [] + components = [c.name for c in bug.get_field("components")] if bug.get_field("components") else [] + + # Check for issue links (for backport detection) + # In Jira, a bug is a backport if it has an outward "Depend" link + # (meaning it "depends on" another bug - the original bug being backported) + issue_links = [] + try: + if hasattr(bug.fields, 'issuelinks') and bug.fields.issuelinks: + for link in bug.fields.issuelinks: + link_type = link.type.name if hasattr(link, 'type') else "Unknown" + if hasattr(link, 'outwardIssue'): + issue_links.append(f"{link_type} -> {link.outwardIssue.key}") + elif hasattr(link, 'inwardIssue'): + issue_links.append(f"{link.inwardIssue.key} -> {link_type}") + except: + pass + + # Get comments + comments = [] + try: + comment_objs = jira_client.comments(bug) + for comment in comment_objs[:5]: # Limit to first 5 comments + author = comment.author.displayName if hasattr(comment.author, 'displayName') else "Unknown" + body = comment.body[:200] + "..." if len(comment.body) > 200 else comment.body + comments.append(f"{author}: {body}") + except: + pass + + print(f"\n{'=' * 80}") + print(f"Bug ID: {bug_id}") + print(f"URL: {bug_url}") + print(f"Summary: {summary}") + print(f"Components: {', '.join(components)}") + print(f"Labels: {', '.join(labels) if labels else 'None'}") + print(f"Issue Links: {', '.join(issue_links) if issue_links else 'None'}") + print(f"\nDescription:\n{description[:500]}{'...' if len(description) > 500 else ''}") + if comments: + print(f"\nComments ({len(comments)} shown):") + for i, comment in enumerate(comments, 1): + print(f" {i}. {comment}") + print(f"{'=' * 80}") + + +def apply_label_to_bug(bug_id, label): + """Apply a label to a specific bug.""" + if not bug_id or not label: + print("Error: Both --label-bug and --label are required") + return + + jira_client = init_jira() + + try: + # Get the current bug to check if it exists + bug = jira_client.issue(bug_id) + current_labels = bug.fields.labels or [] + + # Check if label already exists + if label in current_labels: + print(f"Label '{label}' already exists on {bug_id}") + return + + # Add the new label + new_labels = current_labels + [label] + bug.update(fields={"labels": new_labels}) + + bug_url = get_jira_issue_url(bug_id) + print(f"✓ Successfully applied label '{label}' to {bug_id}") + print(f" URL: {bug_url}") + print(f" Summary: {bug.fields.summary}") + print(f" All labels: {', '.join(new_labels)}") + + except Exception as ex: + print(f"Error applying label to {bug_id}: {ex}") + + def list_old_bot_bugs(): """List bug issues assigned to 'sdn-team-bot' that are older than 4 months""" from datetime import datetime, timedelta @@ -1331,6 +1442,14 @@ def main(): print_bug(params["print_bug"]) return + if params.get("query"): + run_custom_query(params["query"]) + return + + if params.get("label_bug") or params.get("label"): + apply_label_to_bug(params.get("label_bug"), params.get("label")) + return + if params.get("recent_bot_bug_comments"): list_recent_bug_comments() return