Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:45:43 +08:00
commit 77cb91c246
25 changed files with 7424 additions and 0 deletions

385
skills/list-jiras/SKILL.md Normal file
View File

@@ -0,0 +1,385 @@
---
name: List JIRAs
description: Query and return raw JIRA bug data for a specific project
---
# List JIRAs
This skill provides functionality to query JIRA bugs for a specified project and return raw issue data. It uses the JIRA REST API to fetch complete bug information with all fields and metadata, without performing any summarization.
## When to Use This Skill
Use this skill when you need to:
- Fetch raw JIRA issue data for further processing
- Access complete issue details including all fields
- Build custom analysis workflows
- Provide data to other commands (like `summarize-jiras`)
- Export JIRA data for offline analysis
## 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/list-jiras/list_jiras.py
```
### Step 4: Run the Script
Execute the script with appropriate arguments:
```bash
# Basic usage - all open bugs in a project
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS
# Filter by component
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS \
--component "kube-apiserver"
# Filter by multiple components
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS \
--component "kube-apiserver" "Management Console"
# Include closed bugs
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS \
--include-closed
# Filter by status
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS \
--status New "In Progress"
# Set maximum results limit (default 100)
python3 plugins/component-health/skills/list-jiras/list_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
},
"issues": [
{
"key": "OCPBUGS-12345",
"fields": {
"summary": "Bug title here",
"status": {
"name": "New",
"id": "1"
},
"priority": {
"name": "Major",
"id": "3"
},
"components": [
{"name": "kube-apiserver", "id": "12345"}
],
"assignee": {
"displayName": "John Doe",
"emailAddress": "jdoe@example.com"
},
"created": "2025-11-01T10:30:00.000+0000",
"updated": "2025-11-05T14:20:00.000+0000",
"resolutiondate": null,
"versions": [
{"name": "4.21"}
],
"fixVersions": [
{"name": "4.22"}
],
"customfield_12319940": "4.22.0"
}
},
...more issues...
],
"note": "Showing first 100 of 1500 total results. Increase --limit for more data."
}
```
**Field Descriptions**:
- `project`: The JIRA project queried
- `total_count`: Total number of matching issues in JIRA (from 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)
- `issues`: Array of raw JIRA issue objects, each containing:
- `key`: Issue key (e.g., "OCPBUGS-12345")
- `fields`: Object containing all JIRA fields for the issue:
- `summary`: Issue title/summary
- `status`: Status object with name and ID
- `priority`: Priority object with name and ID
- `components`: Array of component objects
- `assignee`: Assignee object with user details
- `created`: Creation timestamp
- `updated`: Last updated timestamp
- `resolutiondate`: Resolution timestamp (null if not closed)
- `versions`: Affects Version/s array
- `fixVersions`: Fix Version/s array
- `customfield_12319940`: Target Version (custom field)
- And many other JIRA fields as applicable
- `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 1000, configurable with `--limit`)
- The `total_count` represents all matching issues in JIRA
- The returned data includes ALL fields for each issue, providing complete information
- For large datasets, increase the `--limit` parameter to fetch more issues
- Issues can have multiple components
- All JIRA field data is preserved in the raw format
### Step 6: Present Results
Based on the raw JIRA data:
1. Inform the user about the total count vs fetched count
2. Explain that the raw data includes all JIRA fields
3. Suggest using `/component-health:summarize-jiras` if they need summary statistics
4. The raw issue data can be passed to other commands for further processing
5. Highlight any truncation and suggest increasing --limit if needed
## Error Handling
### Common Errors
1. **Authentication Errors**
- **Symptom**: HTTP 401 Unauthorized
- **Solution**: Verify JIRA_PERSONAL_TOKEN is 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_USERNAME, 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/list-jiras/list_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 metadata and raw issue data:
```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
},
"issues": [
{
"key": "OCPBUGS-12345",
"fields": {
"summary": "Example bug",
"status": {"name": "New"},
"priority": {"name": "Major"},
"components": [{"name": "kube-apiserver"}],
"created": "2025-11-01T10:30:00.000+0000",
...
}
},
...
],
"note": "Showing first 100 of 5430 total results. Increase --limit for more data."
}
```
## Examples
### Example 1: List All Open Bugs
```bash
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS
```
**Expected Output**: JSON containing raw issue data for all open bugs in OCPBUGS project
### Example 2: Filter by Component
```bash
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS \
--component "kube-apiserver"
```
**Expected Output**: JSON containing raw issue data for the kube-apiserver component only
### Example 3: Include Closed Bugs
```bash
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS \
--include-closed \
--limit 500
```
**Expected Output**: JSON containing raw issue data for both open and closed bugs (up to 500 issues)
### Example 4: Filter by Multiple Components
```bash
python3 plugins/component-health/skills/list-jiras/list_jiras.py \
--project OCPBUGS \
--component "kube-apiserver" "etcd" "Networking"
```
**Expected Output**: JSON containing raw issue data for bugs in specified components
## Integration with Commands
This skill is designed to:
- Provide raw JIRA data to other commands (like `summarize-jiras`)
- Be used directly for ad-hoc JIRA queries
- Serve as a data source for custom analysis workflows
- Export JIRA data for offline processing
## Related Skills
- `summarize-jiras`: Calculate summary statistics from JIRA 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 `urllib` and `json` modules (no external dependencies)
- Output is always JSON format for easy parsing and further processing
- Diagnostic messages are written to stderr, data to stdout
- The script has a 30-second timeout for HTTP requests
- For large projects, consider using component filters to reduce query size
- The returned data includes ALL JIRA fields for complete information
- Use `/component-health:summarize-jiras` if you need summary statistics instead of raw data

View File

@@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""
JIRA Bug Query Script
This script queries JIRA bugs for a specified project and returns raw issue data.
It uses environment variables for authentication and supports filtering by component,
status, and other criteria.
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 list_jiras.py --project OCPBUGS
python3 list_jiras.py --project OCPBUGS --component "kube-apiserver"
python3 list_jiras.py --project OCPBUGS --status New "In Progress"
python3 list_jiras.py --project OCPBUGS --include-closed --limit 500
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
import urllib.parse
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
def get_env_var(name: str) -> str:
"""Get required environment variable or exit with error."""
value = os.environ.get(name)
if not value:
print(f"Error: Environment variable {name} is not set", file=sys.stderr)
print(f"Please set {name} before running this script", file=sys.stderr)
sys.exit(1)
return value
def build_jql_query(project: str, components: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
include_closed: bool = False) -> str:
"""Build JQL query string from parameters."""
parts = [f'project = {project}']
# Calculate date for 30 days ago
thirty_days_ago = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
# Add status filter - include recently closed bugs (within last 30 days) or open bugs
if statuses:
# If specific statuses are requested, use them
status_list = ', '.join(f'"{s}"' for s in statuses)
parts.append(f'status IN ({status_list})')
elif not include_closed:
# Default: open bugs OR bugs closed in the last 30 days
parts.append(f'(status != Closed OR (status = Closed AND resolved >= "{thirty_days_ago}"))')
# If include_closed is True, get all bugs (no status filter)
# Add component filter
if components:
component_list = ', '.join(f'"{c}"' for c in components)
parts.append(f'component IN ({component_list})')
return ' AND '.join(parts)
def fetch_jira_issues(jira_url: str, token: str,
jql: str, max_results: int = 100) -> Dict[str, Any]:
"""
Fetch issues from JIRA using JQL query.
Args:
jira_url: Base JIRA URL
token: JIRA bearer token
jql: JQL query string
max_results: Maximum number of results to fetch
Returns:
Dictionary containing JIRA API response
"""
# Build API URL
api_url = f"{jira_url}/rest/api/2/search"
# Build query parameters - Note: fields should be comma-separated without URL encoding the commas
fields_list = [
'summary', 'status', 'priority', 'components', 'assignee',
'created', 'updated', 'resolutiondate',
'versions', # Affects Version/s
'fixVersions', # Fix Version/s
'customfield_12319940' # Target Version
]
params = {
'jql': jql,
'maxResults': max_results,
'fields': ','.join(fields_list)
}
# Encode parameters - but don't encode commas in fields parameter
encoded_params = []
for k, v in params.items():
if k == 'fields':
# Don't encode commas in fields list
encoded_params.append(f'{k}={v}')
else:
encoded_params.append(f'{k}={urllib.parse.quote(str(v))}')
query_string = '&'.join(encoded_params)
full_url = f"{api_url}?{query_string}"
# Create request with bearer token authentication
request = urllib.request.Request(full_url)
request.add_header('Authorization', f'Bearer {token}')
# Note: Don't add Content-Type for GET requests
print(f"Fetching issues from JIRA...", file=sys.stderr)
print(f"JQL: {jql}", file=sys.stderr)
try:
with urllib.request.urlopen(request, timeout=30) as response:
data = json.loads(response.read().decode())
print(f"Fetched {len(data.get('issues', []))} of {data.get('total', 0)} total issues",
file=sys.stderr)
return data
except urllib.error.HTTPError as e:
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
try:
error_body = e.read().decode()
print(f"Response: {error_body}", file=sys.stderr)
except:
pass
sys.exit(1)
except urllib.error.URLError as e:
print(f"URL Error: {e.reason}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error fetching data: {e}", file=sys.stderr)
sys.exit(1)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='Query JIRA bugs and return raw issue data',
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)
# Get environment variables
jira_url = get_env_var('JIRA_URL').rstrip('/')
token = get_env_var('JIRA_PERSONAL_TOKEN')
# If multiple components are provided, warn user and iterate through them
if args.component and len(args.component) > 1:
print(f"\nQuerying {len(args.component)} components individually...", file=sys.stderr)
print("This may take a few seconds.", file=sys.stderr)
print(f"Components: {', '.join(args.component)}\n", file=sys.stderr)
# Initialize aggregated results
all_issues = []
all_total_count = 0
component_queries = []
# Iterate through each component
for idx, component in enumerate(args.component, 1):
print(f"[{idx}/{len(args.component)}] Querying component: {component}...", file=sys.stderr)
# Build JQL query for this component
jql = build_jql_query(
project=args.project,
components=[component],
statuses=args.status,
include_closed=args.include_closed
)
# Fetch issues for this component
response = fetch_jira_issues(jira_url, token, jql, args.limit)
# Aggregate results
component_issues = response.get('issues', [])
component_total = response.get('total', 0)
all_issues.extend(component_issues)
all_total_count += component_total
component_queries.append(f"{component}: {jql}")
print(f" Found {len(component_issues)} of {component_total} total issues for {component}",
file=sys.stderr)
print(f"\nTotal issues fetched: {len(all_issues)} (from {all_total_count} total across all components)\n",
file=sys.stderr)
# Build combined JQL query string for output (informational only)
combined_jql = build_jql_query(
project=args.project,
components=args.component,
statuses=args.status,
include_closed=args.include_closed
)
# Build output with aggregated data
output = {
'project': args.project,
'total_count': all_total_count,
'fetched_count': len(all_issues),
'query': combined_jql,
'component_queries': component_queries,
'filters': {
'components': args.component,
'statuses': args.status,
'include_closed': args.include_closed,
'limit': args.limit
},
'issues': all_issues
}
# Add note if results are truncated
if len(all_issues) < all_total_count:
output['note'] = (
f"Showing {len(all_issues)} of {all_total_count} total results across {len(args.component)} components. "
f"Increase --limit to fetch more per component."
)
else:
# Single component or no component filter - use original logic
# Build JQL query
jql = build_jql_query(
project=args.project,
components=args.component,
statuses=args.status,
include_closed=args.include_closed
)
# Fetch issues
response = fetch_jira_issues(jira_url, token, jql, args.limit)
# Extract data
issues = response.get('issues', [])
total_count = response.get('total', 0)
fetched_count = len(issues)
# Build output with metadata and raw issues
output = {
'project': args.project,
'total_count': total_count,
'fetched_count': fetched_count,
'query': jql,
'filters': {
'components': args.component,
'statuses': args.status,
'include_closed': args.include_closed,
'limit': args.limit
},
'issues': issues
}
# Add note if results are truncated
if fetched_count < total_count:
output['note'] = (
f"Showing first {fetched_count} of {total_count} total results. "
f"Increase --limit for more data."
)
# Output JSON to stdout
print(json.dumps(output, indent=2))
if __name__ == '__main__':
main()