Initial commit
This commit is contained in:
440
skills/summarize-jiras/SKILL.md
Normal file
440
skills/summarize-jiras/SKILL.md
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
name: Summarize JIRAs
|
||||
description: Query and summarize JIRA bugs for a specific project with counts by component
|
||||
---
|
||||
|
||||
# Summarize JIRAs
|
||||
|
||||
This skill provides functionality to query JIRA bugs for a specified project and generate summary statistics. It leverages the `list-jiras` skill to fetch raw JIRA data, then calculates counts by status, priority, and component to provide insights into the bug backlog.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
|
||||
- Get a count of open bugs in a JIRA project
|
||||
- Analyze bug distribution by status, priority, or component
|
||||
- Generate summary reports for bug backlog
|
||||
- Track bug trends and velocity over time (opened vs closed in last 30 days)
|
||||
- Compare bug counts across different components
|
||||
- Monitor component health based on bug metrics
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Python 3 Installation**
|
||||
- Check if installed: `which python3`
|
||||
- Python 3.6 or later is required
|
||||
- Comes pre-installed on most systems
|
||||
|
||||
2. **JIRA Authentication**
|
||||
- Requires environment variables to be set:
|
||||
- `JIRA_URL`: Base URL for JIRA instance (e.g., "https://issues.redhat.com")
|
||||
- `JIRA_PERSONAL_TOKEN`: Your JIRA bearer token or personal access token
|
||||
- How to get a JIRA token:
|
||||
- Navigate to JIRA → Profile → Personal Access Tokens
|
||||
- Generate a new token with appropriate permissions
|
||||
- Export it as an environment variable
|
||||
|
||||
3. **Network Access**
|
||||
- The script requires network access to reach your JIRA instance
|
||||
- Ensure you can make HTTPS requests to the JIRA URL
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Verify Prerequisites
|
||||
|
||||
First, ensure Python 3 is available:
|
||||
|
||||
```bash
|
||||
python3 --version
|
||||
```
|
||||
|
||||
If Python 3 is not installed, guide the user through installation for their platform.
|
||||
|
||||
### Step 2: Verify Environment Variables
|
||||
|
||||
Check that required environment variables are set:
|
||||
|
||||
```bash
|
||||
# Verify JIRA credentials are configured
|
||||
echo "JIRA_URL: ${JIRA_URL}"
|
||||
echo "JIRA_PERSONAL_TOKEN: ${JIRA_PERSONAL_TOKEN:+***set***}"
|
||||
```
|
||||
|
||||
If any are missing, guide the user to set them:
|
||||
|
||||
```bash
|
||||
export JIRA_URL="https://issues.redhat.com"
|
||||
export JIRA_PERSONAL_TOKEN="your-token-here"
|
||||
```
|
||||
|
||||
### Step 3: Locate the Script
|
||||
|
||||
The script is located at:
|
||||
|
||||
```
|
||||
plugins/component-health/skills/summarize-jiras/summarize_jiras.py
|
||||
```
|
||||
|
||||
### Step 4: Run the Script
|
||||
|
||||
Execute the script with appropriate arguments:
|
||||
|
||||
```bash
|
||||
# Basic usage - summarize all open bugs in a project
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS
|
||||
|
||||
# Filter by component
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--component "kube-apiserver"
|
||||
|
||||
# Filter by multiple components
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--component "kube-apiserver" "Management Console"
|
||||
|
||||
# Include closed bugs
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--include-closed
|
||||
|
||||
# Filter by status
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--status New "In Progress"
|
||||
|
||||
# Set maximum results limit (default 100)
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--limit 500
|
||||
```
|
||||
|
||||
### Step 5: Process the Output
|
||||
|
||||
The script outputs JSON data with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "OCPBUGS",
|
||||
"total_count": 1500,
|
||||
"fetched_count": 100,
|
||||
"query": "project = OCPBUGS AND (status != Closed OR (status = Closed AND resolved >= \"2025-10-11\"))",
|
||||
"filters": {
|
||||
"components": null,
|
||||
"statuses": null,
|
||||
"include_closed": false,
|
||||
"limit": 100
|
||||
},
|
||||
"summary": {
|
||||
"total": 100,
|
||||
"opened_last_30_days": 15,
|
||||
"closed_last_30_days": 8,
|
||||
"by_status": {
|
||||
"New": 35,
|
||||
"In Progress": 25,
|
||||
"Verified": 20,
|
||||
"Modified": 15,
|
||||
"ON_QA": 5,
|
||||
"Closed": 8
|
||||
},
|
||||
"by_priority": {
|
||||
"Normal": 50,
|
||||
"Major": 30,
|
||||
"Minor": 12,
|
||||
"Critical": 5,
|
||||
"Undefined": 3
|
||||
},
|
||||
"by_component": {
|
||||
"kube-apiserver": 25,
|
||||
"Management Console": 30,
|
||||
"Networking": 20,
|
||||
"etcd": 15,
|
||||
"No Component": 10
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"kube-apiserver": {
|
||||
"total": 25,
|
||||
"opened_last_30_days": 4,
|
||||
"closed_last_30_days": 2,
|
||||
"by_status": {
|
||||
"New": 10,
|
||||
"In Progress": 8,
|
||||
"Verified": 5,
|
||||
"Modified": 2,
|
||||
"Closed": 2
|
||||
},
|
||||
"by_priority": {
|
||||
"Major": 12,
|
||||
"Normal": 10,
|
||||
"Minor": 2,
|
||||
"Critical": 1
|
||||
}
|
||||
},
|
||||
"Management Console": {
|
||||
"total": 30,
|
||||
"opened_last_30_days": 6,
|
||||
"closed_last_30_days": 3,
|
||||
"by_status": {
|
||||
"New": 12,
|
||||
"In Progress": 10,
|
||||
"Verified": 6,
|
||||
"Modified": 2,
|
||||
"Closed": 3
|
||||
},
|
||||
"by_priority": {
|
||||
"Normal": 18,
|
||||
"Major": 8,
|
||||
"Minor": 3,
|
||||
"Critical": 1
|
||||
}
|
||||
},
|
||||
"etcd": {
|
||||
"total": 15,
|
||||
"opened_last_30_days": 3,
|
||||
"closed_last_30_days": 2,
|
||||
"by_status": {
|
||||
"New": 8,
|
||||
"In Progress": 4,
|
||||
"Verified": 3,
|
||||
"Closed": 2
|
||||
},
|
||||
"by_priority": {
|
||||
"Normal": 10,
|
||||
"Major": 4,
|
||||
"Critical": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"note": "Showing first 100 of 1500 total results. Increase --limit for more accurate statistics."
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions**:
|
||||
|
||||
- `project`: The JIRA project queried
|
||||
- `total_count`: Total number of matching issues (from JIRA search results)
|
||||
- `fetched_count`: Number of issues actually fetched (limited by --limit parameter)
|
||||
- `query`: The JQL query executed (includes filter for recently closed bugs)
|
||||
- `filters`: Applied filters (components, statuses, include_closed, limit)
|
||||
- `summary`: Overall statistics across all fetched issues
|
||||
- `total`: Count of fetched issues (same as `fetched_count`)
|
||||
- `opened_last_30_days`: Number of issues created in the last 30 days
|
||||
- `closed_last_30_days`: Number of issues closed/resolved in the last 30 days
|
||||
- `by_status`: Count of issues per status (includes recently closed issues)
|
||||
- `by_priority`: Count of issues per priority
|
||||
- `by_component`: Count of issues per component (note: issues can have multiple components)
|
||||
- `components`: Per-component breakdown with individual summaries
|
||||
- Each component key maps to:
|
||||
- `total`: Number of issues assigned to this component
|
||||
- `opened_last_30_days`: Number of issues created in the last 30 days for this component
|
||||
- `closed_last_30_days`: Number of issues closed in the last 30 days for this component
|
||||
- `by_status`: Status distribution for this component
|
||||
- `by_priority`: Priority distribution for this component
|
||||
- `note`: Informational message if results are truncated
|
||||
|
||||
**Important Notes**:
|
||||
|
||||
- **By default, the query includes**: Open bugs + bugs closed in the last 30 days
|
||||
- This allows tracking of recent closure activity alongside current open bugs
|
||||
- The script fetches a maximum number of issues (default 100, configurable with `--limit`)
|
||||
- The `total_count` represents all matching issues in JIRA
|
||||
- Summary statistics are based on the fetched issues only
|
||||
- For accurate statistics across large datasets, increase the `--limit` parameter
|
||||
- Issues can have multiple components, so component totals may sum to more than the overall total
|
||||
- `opened_last_30_days` and `closed_last_30_days` help track recent bug flow and velocity
|
||||
|
||||
### Step 6: Present Results
|
||||
|
||||
Based on the summary data:
|
||||
|
||||
1. Present total bug counts
|
||||
2. Highlight distribution by status (e.g., how many in "New" vs "In Progress")
|
||||
3. Identify priority breakdown (Critical, Major, Normal, etc.)
|
||||
4. Show component distribution
|
||||
5. Display per-component breakdowns with status and priority counts
|
||||
6. Calculate actionable metrics (e.g., New + Assigned = bugs needing triage/work)
|
||||
7. Highlight recent activity (opened/closed in last 30 days) per component
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
1. **Authentication Errors**
|
||||
- **Symptom**: HTTP 401 Unauthorized
|
||||
- **Solution**: Verify JIRA_URL and JIRA_PERSONAL_TOKEN are correct
|
||||
- **Check**: Ensure token has not expired
|
||||
|
||||
2. **Network Errors**
|
||||
- **Symptom**: `URLError` or connection timeout
|
||||
- **Solution**: Check network connectivity and JIRA_URL is accessible
|
||||
- **Retry**: The script has a 30-second timeout, consider retrying
|
||||
|
||||
3. **Invalid Project**
|
||||
- **Symptom**: HTTP 400 or empty results
|
||||
- **Solution**: Verify the project key is correct (e.g., "OCPBUGS", not "ocpbugs")
|
||||
|
||||
4. **Missing Environment Variables**
|
||||
- **Symptom**: Error message about missing credentials
|
||||
- **Solution**: Set required environment variables (JIRA_URL, JIRA_PERSONAL_TOKEN)
|
||||
|
||||
5. **Rate Limiting**
|
||||
- **Symptom**: HTTP 429 Too Many Requests
|
||||
- **Solution**: Wait before retrying, reduce query frequency
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable verbose output by examining stderr:
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS 2>&1 | tee debug.log
|
||||
```
|
||||
|
||||
## Script Arguments
|
||||
|
||||
### Required Arguments
|
||||
|
||||
- `--project`: JIRA project key to query
|
||||
- Format: Project key (e.g., "OCPBUGS", "OCPSTRAT")
|
||||
- Must be a valid JIRA project
|
||||
|
||||
### Optional Arguments
|
||||
|
||||
- `--component`: Filter by component names
|
||||
- Values: Space-separated list of component names
|
||||
- Default: None (returns all components)
|
||||
- Case-sensitive matching
|
||||
- Examples: `--component "kube-apiserver" "Management Console"`
|
||||
|
||||
- `--status`: Filter by status values
|
||||
- Values: Space-separated list of status names
|
||||
- Default: None (returns all statuses except Closed)
|
||||
- Examples: `--status New "In Progress" Verified`
|
||||
|
||||
- `--include-closed`: Include closed bugs in the results
|
||||
- Default: false (only open bugs)
|
||||
- When specified, includes bugs in "Closed" status
|
||||
|
||||
- `--limit`: Maximum number of issues to fetch
|
||||
- Default: 100
|
||||
- Maximum: 1000 (JIRA API limit per request)
|
||||
- Higher values provide more accurate statistics but slower performance
|
||||
|
||||
## Output Format
|
||||
|
||||
The script outputs JSON with summary statistics and per-component breakdowns:
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "OCPBUGS",
|
||||
"total_count": 5430,
|
||||
"fetched_count": 100,
|
||||
"query": "project = OCPBUGS AND (status != Closed OR (status = Closed AND resolved >= \"2025-10-11\"))",
|
||||
"filters": {
|
||||
"components": null,
|
||||
"statuses": null,
|
||||
"include_closed": false,
|
||||
"limit": 100
|
||||
},
|
||||
"summary": {
|
||||
"total": 100,
|
||||
"opened_last_30_days": 15,
|
||||
"closed_last_30_days": 8,
|
||||
"by_status": {
|
||||
"New": 1250,
|
||||
"In Progress": 800,
|
||||
"Verified": 650
|
||||
},
|
||||
"by_priority": {
|
||||
"Critical": 50,
|
||||
"Major": 450,
|
||||
"Normal": 2100
|
||||
},
|
||||
"by_component": {
|
||||
"kube-apiserver": 146,
|
||||
"Management Console": 392
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"kube-apiserver": {
|
||||
"total": 146,
|
||||
"opened_last_30_days": 20,
|
||||
"closed_last_30_days": 12,
|
||||
"by_status": {...},
|
||||
"by_priority": {...}
|
||||
}
|
||||
},
|
||||
"note": "Showing first 100 of 5430 total results. Increase --limit for more accurate statistics."
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Summarize All Open Bugs
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS
|
||||
```
|
||||
|
||||
**Expected Output**: JSON containing summary statistics of all open bugs in OCPBUGS project
|
||||
|
||||
### Example 2: Filter by Component
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--component "kube-apiserver"
|
||||
```
|
||||
|
||||
**Expected Output**: JSON containing summary for the kube-apiserver component only
|
||||
|
||||
### Example 3: Include Closed Bugs
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--include-closed \
|
||||
--limit 500
|
||||
```
|
||||
|
||||
**Expected Output**: JSON containing summary of both open and closed bugs (up to 500 issues)
|
||||
|
||||
### Example 4: Filter by Multiple Components
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/summarize-jiras/summarize_jiras.py \
|
||||
--project OCPBUGS \
|
||||
--component "kube-apiserver" "etcd" "Networking"
|
||||
```
|
||||
|
||||
**Expected Output**: JSON containing summary for specified components
|
||||
|
||||
## Integration with Commands
|
||||
|
||||
This skill is designed to:
|
||||
- Provide summary statistics for JIRA bug analysis
|
||||
- Be used by component health analysis workflows
|
||||
- Generate reports for bug triage and planning
|
||||
- Track component health metrics over time
|
||||
- Leverage the `list-jiras` skill for raw data fetching
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `list-jiras`: Fetch raw JIRA issue data
|
||||
- `list-regressions`: Fetch regression data for releases
|
||||
- `analyze-regressions`: Grade component health based on regressions
|
||||
- `get-release-dates`: Fetch OpenShift release dates
|
||||
|
||||
## Notes
|
||||
|
||||
- The script uses Python's standard library only (no external dependencies)
|
||||
- Output is always JSON format for easy parsing
|
||||
- Diagnostic messages are written to stderr, data to stdout
|
||||
- The script internally calls `list_jiras.py` to fetch raw data
|
||||
- The script has a 30-second timeout for HTTP requests (inherited from list_jiras.py)
|
||||
- For large projects, consider using component filters to reduce query size
|
||||
- Summary statistics are based on fetched issues (controlled by --limit), not total matching issues
|
||||
- For raw JIRA data without summarization, use `/component-health:list-jiras` instead
|
||||
362
skills/summarize-jiras/summarize_jiras.py
Executable file
362
skills/summarize-jiras/summarize_jiras.py
Executable file
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JIRA Bug Summarization Script
|
||||
|
||||
This script queries JIRA bugs for a specified project and generates summary statistics.
|
||||
It leverages the list_jiras.py script to fetch raw data, then calculates counts by
|
||||
status, priority, and component.
|
||||
|
||||
Environment Variables:
|
||||
JIRA_URL: Base URL for JIRA instance (e.g., "https://issues.redhat.com")
|
||||
JIRA_PERSONAL_TOKEN: Your JIRA API bearer token or personal access token
|
||||
|
||||
Usage:
|
||||
python3 summarize_jiras.py --project OCPBUGS
|
||||
python3 summarize_jiras.py --project OCPBUGS --component "kube-apiserver"
|
||||
python3 summarize_jiras.py --project OCPBUGS --status New "In Progress"
|
||||
python3 summarize_jiras.py --project OCPBUGS --include-closed --limit 500
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from typing import List, Dict, Any
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def call_list_jiras(project: str, components: List[str] = None,
|
||||
statuses: List[str] = None,
|
||||
include_closed: bool = False,
|
||||
limit: int = 100) -> Dict[str, Any]:
|
||||
"""
|
||||
Call the list_jiras.py script to fetch raw JIRA data.
|
||||
|
||||
Args:
|
||||
project: JIRA project key
|
||||
components: Optional list of component names to filter by
|
||||
statuses: Optional list of status values to filter by
|
||||
include_closed: Whether to include closed bugs
|
||||
limit: Maximum number of issues to fetch
|
||||
|
||||
Returns:
|
||||
Dictionary containing raw JIRA data from list_jiras.py
|
||||
"""
|
||||
# Build command to call list_jiras.py
|
||||
script_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
'list-jiras',
|
||||
'list_jiras.py'
|
||||
)
|
||||
|
||||
cmd = ['python3', script_path, '--project', project, '--limit', str(limit)]
|
||||
|
||||
if components:
|
||||
cmd.append('--component')
|
||||
cmd.extend(components)
|
||||
|
||||
if statuses:
|
||||
cmd.append('--status')
|
||||
cmd.extend(statuses)
|
||||
|
||||
if include_closed:
|
||||
cmd.append('--include-closed')
|
||||
|
||||
print(f"Calling list_jiras.py to fetch raw data...", file=sys.stderr)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=300 # 5 minutes to allow for multiple component queries
|
||||
)
|
||||
# Pass through stderr to show progress messages from list_jiras.py
|
||||
if result.stderr:
|
||||
print(result.stderr, file=sys.stderr, end='')
|
||||
return json.loads(result.stdout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error calling list_jiras.py: {e}", file=sys.stderr)
|
||||
if e.stderr:
|
||||
print(f"Error output: {e.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"Timeout calling list_jiras.py (exceeded 5 minutes)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JSON from list_jiras.py: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def generate_summary(issues: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate summary statistics from issues.
|
||||
|
||||
Args:
|
||||
issues: List of JIRA issue objects
|
||||
|
||||
Returns:
|
||||
Dictionary containing overall summary and per-component summaries
|
||||
"""
|
||||
# Calculate cutoff dates
|
||||
now = datetime.now()
|
||||
thirty_days_ago = now - timedelta(days=30)
|
||||
ninety_days_ago = now - timedelta(days=90)
|
||||
one_eighty_days_ago = now - timedelta(days=180)
|
||||
|
||||
# Overall summary
|
||||
overall_summary = {
|
||||
'total': 0,
|
||||
'opened_last_30_days': 0,
|
||||
'closed_last_30_days': 0,
|
||||
'by_status': defaultdict(int),
|
||||
'by_priority': defaultdict(int),
|
||||
'by_component': defaultdict(int),
|
||||
'open_bugs_by_age': {
|
||||
'0-30d': 0,
|
||||
'30-90d': 0,
|
||||
'90-180d': 0,
|
||||
'>180d': 0
|
||||
}
|
||||
}
|
||||
|
||||
# Per-component data
|
||||
components_data = defaultdict(lambda: {
|
||||
'total': 0,
|
||||
'opened_last_30_days': 0,
|
||||
'closed_last_30_days': 0,
|
||||
'by_status': defaultdict(int),
|
||||
'by_priority': defaultdict(int),
|
||||
'open_bugs_by_age': {
|
||||
'0-30d': 0,
|
||||
'30-90d': 0,
|
||||
'90-180d': 0,
|
||||
'>180d': 0
|
||||
}
|
||||
})
|
||||
|
||||
for issue in issues:
|
||||
fields = issue.get('fields', {})
|
||||
overall_summary['total'] += 1
|
||||
|
||||
# Parse created date
|
||||
created_str = fields.get('created')
|
||||
if created_str:
|
||||
try:
|
||||
# JIRA date format: 2024-01-15T10:30:00.000+0000
|
||||
created_date = datetime.strptime(created_str[:19], '%Y-%m-%dT%H:%M:%S')
|
||||
if created_date >= thirty_days_ago:
|
||||
overall_summary['opened_last_30_days'] += 1
|
||||
is_recently_opened = True
|
||||
else:
|
||||
is_recently_opened = False
|
||||
except (ValueError, TypeError):
|
||||
is_recently_opened = False
|
||||
else:
|
||||
is_recently_opened = False
|
||||
|
||||
# Parse resolution date (when issue was closed)
|
||||
resolution_date_str = fields.get('resolutiondate')
|
||||
if resolution_date_str:
|
||||
try:
|
||||
resolution_date = datetime.strptime(resolution_date_str[:19], '%Y-%m-%dT%H:%M:%S')
|
||||
if resolution_date >= thirty_days_ago:
|
||||
overall_summary['closed_last_30_days'] += 1
|
||||
is_recently_closed = True
|
||||
else:
|
||||
is_recently_closed = False
|
||||
except (ValueError, TypeError):
|
||||
is_recently_closed = False
|
||||
else:
|
||||
is_recently_closed = False
|
||||
|
||||
# Count by status
|
||||
status = fields.get('status', {}).get('name', 'Unknown')
|
||||
overall_summary['by_status'][status] += 1
|
||||
|
||||
# Count by priority
|
||||
priority = fields.get('priority')
|
||||
if priority:
|
||||
priority_name = priority.get('name', 'Undefined')
|
||||
else:
|
||||
priority_name = 'Undefined'
|
||||
overall_summary['by_priority'][priority_name] += 1
|
||||
|
||||
# Calculate age for open bugs
|
||||
is_open = status != 'Closed'
|
||||
age_bucket = None
|
||||
if is_open and created_str:
|
||||
try:
|
||||
created_date = datetime.strptime(created_str[:19], '%Y-%m-%dT%H:%M:%S')
|
||||
age_days = (now - created_date).days
|
||||
|
||||
if age_days <= 30:
|
||||
age_bucket = '0-30d'
|
||||
elif age_days <= 90:
|
||||
age_bucket = '30-90d'
|
||||
elif age_days <= 180:
|
||||
age_bucket = '90-180d'
|
||||
else:
|
||||
age_bucket = '>180d'
|
||||
|
||||
overall_summary['open_bugs_by_age'][age_bucket] += 1
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Process components (issues can have multiple components)
|
||||
components = fields.get('components', [])
|
||||
component_names = []
|
||||
|
||||
if components:
|
||||
for component in components:
|
||||
component_name = component.get('name', 'Unknown')
|
||||
component_names.append(component_name)
|
||||
overall_summary['by_component'][component_name] += 1
|
||||
else:
|
||||
component_names = ['No Component']
|
||||
overall_summary['by_component']['No Component'] += 1
|
||||
|
||||
# Update per-component statistics
|
||||
for component_name in component_names:
|
||||
components_data[component_name]['total'] += 1
|
||||
components_data[component_name]['by_status'][status] += 1
|
||||
components_data[component_name]['by_priority'][priority_name] += 1
|
||||
if is_recently_opened:
|
||||
components_data[component_name]['opened_last_30_days'] += 1
|
||||
if is_recently_closed:
|
||||
components_data[component_name]['closed_last_30_days'] += 1
|
||||
if age_bucket:
|
||||
components_data[component_name]['open_bugs_by_age'][age_bucket] += 1
|
||||
|
||||
# Convert defaultdicts to regular dicts and sort
|
||||
overall_summary['by_status'] = dict(sorted(
|
||||
overall_summary['by_status'].items(),
|
||||
key=lambda x: x[1], reverse=True
|
||||
))
|
||||
overall_summary['by_priority'] = dict(sorted(
|
||||
overall_summary['by_priority'].items(),
|
||||
key=lambda x: x[1], reverse=True
|
||||
))
|
||||
overall_summary['by_component'] = dict(sorted(
|
||||
overall_summary['by_component'].items(),
|
||||
key=lambda x: x[1], reverse=True
|
||||
))
|
||||
|
||||
# Convert component data to regular dicts and sort
|
||||
components = {}
|
||||
for comp_name, comp_data in sorted(components_data.items()):
|
||||
components[comp_name] = {
|
||||
'total': comp_data['total'],
|
||||
'opened_last_30_days': comp_data['opened_last_30_days'],
|
||||
'closed_last_30_days': comp_data['closed_last_30_days'],
|
||||
'by_status': dict(sorted(
|
||||
comp_data['by_status'].items(),
|
||||
key=lambda x: x[1], reverse=True
|
||||
)),
|
||||
'by_priority': dict(sorted(
|
||||
comp_data['by_priority'].items(),
|
||||
key=lambda x: x[1], reverse=True
|
||||
)),
|
||||
'open_bugs_by_age': comp_data['open_bugs_by_age']
|
||||
}
|
||||
|
||||
return {
|
||||
'summary': overall_summary,
|
||||
'components': components
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Query JIRA bugs and generate summary statistics',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s --project OCPBUGS
|
||||
%(prog)s --project OCPBUGS --component "kube-apiserver"
|
||||
%(prog)s --project OCPBUGS --component "kube-apiserver" "etcd"
|
||||
%(prog)s --project OCPBUGS --status New "In Progress"
|
||||
%(prog)s --project OCPBUGS --include-closed --limit 500
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--project',
|
||||
required=True,
|
||||
help='JIRA project key (e.g., OCPBUGS, OCPSTRAT)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--component',
|
||||
nargs='+',
|
||||
help='Filter by component names (space-separated)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--status',
|
||||
nargs='+',
|
||||
help='Filter by status values (space-separated)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--include-closed',
|
||||
action='store_true',
|
||||
help='Include closed bugs in results (default: only open bugs)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
type=int,
|
||||
default=1000,
|
||||
help='Maximum number of issues to fetch per component (default: 1000, max: 1000)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate limit
|
||||
if args.limit < 1 or args.limit > 1000:
|
||||
print("Error: --limit must be between 1 and 1000", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Fetch raw JIRA data using list_jiras.py
|
||||
print(f"Fetching JIRA data for project {args.project}...", file=sys.stderr)
|
||||
raw_data = call_list_jiras(
|
||||
project=args.project,
|
||||
components=args.component,
|
||||
statuses=args.status,
|
||||
include_closed=args.include_closed,
|
||||
limit=args.limit
|
||||
)
|
||||
|
||||
# Extract issues from raw data
|
||||
issues = raw_data.get('issues', [])
|
||||
print(f"Generating summary statistics from {len(issues)} issues...", file=sys.stderr)
|
||||
|
||||
# Generate summary statistics
|
||||
summary_data = generate_summary(issues)
|
||||
|
||||
# Build output with metadata and summaries
|
||||
output = {
|
||||
'project': raw_data.get('project'),
|
||||
'total_count': raw_data.get('total_count'),
|
||||
'fetched_count': raw_data.get('fetched_count'),
|
||||
'query': raw_data.get('query'),
|
||||
'filters': raw_data.get('filters'),
|
||||
'summary': summary_data['summary'],
|
||||
'components': summary_data['components']
|
||||
}
|
||||
|
||||
# Add note if present in raw data
|
||||
if 'note' in raw_data:
|
||||
output['note'] = raw_data['note']
|
||||
|
||||
# Output JSON to stdout
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user