Initial commit
This commit is contained in:
100
skills/list-regressions/README.md
Normal file
100
skills/list-regressions/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# List Regressions Skill
|
||||
|
||||
Python script for fetching component health regression data for OpenShift releases.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides a Python script that queries a component health API to retrieve regression information for specific OpenShift releases. The data can be filtered by component names.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# List all regressions for a release
|
||||
python3 list_regressions.py --release 4.17
|
||||
|
||||
# Filter by specific components
|
||||
python3 list_regressions.py --release 4.21 --components Monitoring etcd
|
||||
|
||||
# Filter by single component
|
||||
python3 list_regressions.py --release 4.21 --components "kube-apiserver"
|
||||
|
||||
# Filter to development window (GA'd release - both start and end)
|
||||
python3 list_regressions.py --release 4.17 --start 2024-05-17 --end 2024-10-01
|
||||
|
||||
# Filter to development window (in-development release - start only, no GA yet)
|
||||
python3 list_regressions.py --release 4.21 --start 2025-09-02
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
- `--release` (required): OpenShift release version (e.g., "4.17", "4.16")
|
||||
- `--components` (optional): Space-separated list of component names to filter by (case-insensitive)
|
||||
- `--start` (optional): Start date in YYYY-MM-DD format. Excludes regressions closed before this date.
|
||||
- `--end` (optional): End date in YYYY-MM-DD format. Excludes regressions opened after this date.
|
||||
|
||||
## Output
|
||||
|
||||
The script outputs JSON data with the following structure to stdout:
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {...},
|
||||
"components": {
|
||||
"ComponentName": {
|
||||
"summary": {...},
|
||||
"open": [...],
|
||||
"closed": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Diagnostic messages are written to stderr.
|
||||
|
||||
**Note**:
|
||||
|
||||
- Regressions are grouped by component name (sorted alphabetically)
|
||||
- Each component maps to an object containing:
|
||||
- `summary`: Per-component statistics (total, open, closed, triaged counts, average time to triage)
|
||||
- `open`: Array of open regression objects
|
||||
- `closed`: Array of closed regression objects
|
||||
- Time fields are automatically simplified:
|
||||
- `closed`: Shows timestamp string if closed (e.g., `"2025-09-27T12:04:24.966914Z"`), otherwise `null`
|
||||
- `last_failure`: Shows timestamp string if valid (e.g., `"2025-09-25T14:41:17Z"`), otherwise `null`
|
||||
- Unnecessary fields removed for response size optimization:
|
||||
- `links`: Removed from each regression
|
||||
- `test_id`: Removed from each regression
|
||||
- Optional date filtering to focus on development window:
|
||||
- Use `--start` and `--end` to filter regressions to a specific time period
|
||||
- Typical use: Filter to development window using release dates
|
||||
- `--start`: Always applied (development_start date)
|
||||
- `--end`: Only for GA'd releases (GA date)
|
||||
- For GA'd releases: Both start and end filtering applied
|
||||
- For in-development releases: Only start filtering applied (no end date yet)
|
||||
- Triaged counts: Number of regressions with non-empty `triages` list (triaged to JIRA bugs)
|
||||
- Average time to triage: Average hours from regression opened to earliest triage timestamp (null if no triaged regressions)
|
||||
- Maximum time to triage: Maximum hours from regression opened to earliest triage timestamp (null if no triaged regressions)
|
||||
- Average open duration: Average hours that open regressions have been open (from opened to current time, only for open regressions)
|
||||
- Maximum open duration: Maximum hours that open regressions have been open (from opened to current time, only for open regressions)
|
||||
- Average time to close: Average hours from regression opened to closed timestamp (null if no valid data, only for closed regressions)
|
||||
- Maximum time to close: Maximum hours from regression opened to closed timestamp (null if no valid data, only for closed regressions)
|
||||
- Average time triaged to closed: Average hours from first triage to closed timestamp (null if no valid data, only for triaged closed regressions)
|
||||
- Maximum time triaged to closed: Maximum hours from first triage to closed timestamp (null if no valid data, only for triaged closed regressions)
|
||||
|
||||
## Configuration
|
||||
|
||||
Before using, update the API endpoint in `list_regressions.py`:
|
||||
|
||||
```python
|
||||
base_url = f"https://your-actual-api.example.com/api/v1/regressions"
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6 or later
|
||||
- Network access to the component health API
|
||||
- No external Python dependencies (uses standard library only)
|
||||
|
||||
## See Also
|
||||
|
||||
- [SKILL.md](./SKILL.md) - Detailed implementation guide for AI agents
|
||||
579
skills/list-regressions/SKILL.md
Normal file
579
skills/list-regressions/SKILL.md
Normal file
@@ -0,0 +1,579 @@
|
||||
---
|
||||
name: List Regressions
|
||||
description: Fetch and analyze component health regressions for OpenShift releases
|
||||
---
|
||||
|
||||
# List Regressions
|
||||
|
||||
This skill provides functionality to fetch regression data for OpenShift components across different releases. It uses a Python script to query a component health API and retrieve regression information.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
|
||||
- Analyze component health for a specific OpenShift release
|
||||
- Track regressions across releases
|
||||
- Filter regressions by their open/closed status
|
||||
- Generate reports on component stability
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Python 3 Installation**
|
||||
|
||||
- Check if installed: `which python3`
|
||||
- Python 3.6 or later is required
|
||||
- Comes pre-installed on most systems
|
||||
|
||||
2. **Network Access**
|
||||
|
||||
- The script requires network access to reach the component health API
|
||||
- Ensure you can make HTTPS requests
|
||||
|
||||
3. **API Endpoint Configuration**
|
||||
- The script includes a placeholder API endpoint that needs to be updated
|
||||
- Update the `base_url` in `list_regressions.py` with the actual component health API endpoint
|
||||
|
||||
## 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: Locate the Script
|
||||
|
||||
The script is located at:
|
||||
|
||||
```
|
||||
plugins/component-health/skills/list-regressions/list_regressions.py
|
||||
```
|
||||
|
||||
### Step 3: Run the Script
|
||||
|
||||
Execute the script with appropriate arguments:
|
||||
|
||||
```bash
|
||||
# Basic usage - all regressions for a release
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.17
|
||||
|
||||
# Filter by specific components
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.21 \
|
||||
--components Monitoring "kube-apiserver"
|
||||
|
||||
# Filter by multiple components
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.21 \
|
||||
--components Monitoring etcd "kube-apiserver"
|
||||
|
||||
# Filter by development window (GA'd release - both start and end)
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.17 \
|
||||
--start 2024-05-17 \
|
||||
--end 2024-10-01
|
||||
|
||||
# Filter by development window (in-development release - start only)
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.21 \
|
||||
--start 2025-09-02
|
||||
```
|
||||
|
||||
### Step 4: Process the Output
|
||||
|
||||
The script outputs JSON data with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"total": <number>,
|
||||
"triaged": <number>,
|
||||
"triage_percentage": <number>,
|
||||
"time_to_triage_hrs_avg": <number or null>,
|
||||
"time_to_triage_hrs_max": <number or null>,
|
||||
"time_to_close_hrs_avg": <number or null>,
|
||||
"time_to_close_hrs_max": <number or null>,
|
||||
"open": {
|
||||
"total": <number>,
|
||||
"triaged": <number>,
|
||||
"triage_percentage": <number>,
|
||||
"time_to_triage_hrs_avg": <number or null>,
|
||||
"time_to_triage_hrs_max": <number or null>,
|
||||
"open_hrs_avg": <number or null>,
|
||||
"open_hrs_max": <number or null>
|
||||
},
|
||||
"closed": {
|
||||
"total": <number>,
|
||||
"triaged": <number>,
|
||||
"triage_percentage": <number>,
|
||||
"time_to_triage_hrs_avg": <number or null>,
|
||||
"time_to_triage_hrs_max": <number or null>,
|
||||
"time_to_close_hrs_avg": <number or null>,
|
||||
"time_to_close_hrs_max": <number or null>,
|
||||
"time_triaged_closed_hrs_avg": <number or null>,
|
||||
"time_triaged_closed_hrs_max": <number or null>
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"ComponentName": {
|
||||
"summary": {
|
||||
"total": <number>,
|
||||
"triaged": <number>,
|
||||
"triage_percentage": <number>,
|
||||
"time_to_triage_hrs_avg": <number or null>,
|
||||
"time_to_triage_hrs_max": <number or null>,
|
||||
"time_to_close_hrs_avg": <number or null>,
|
||||
"time_to_close_hrs_max": <number or null>,
|
||||
"open": {
|
||||
"total": <number>,
|
||||
"triaged": <number>,
|
||||
"triage_percentage": <number>,
|
||||
"time_to_triage_hrs_avg": <number or null>,
|
||||
"time_to_triage_hrs_max": <number or null>,
|
||||
"open_hrs_avg": <number or null>,
|
||||
"open_hrs_max": <number or null>
|
||||
},
|
||||
"closed": {
|
||||
"total": <number>,
|
||||
"triaged": <number>,
|
||||
"triage_percentage": <number>,
|
||||
"time_to_triage_hrs_avg": <number or null>,
|
||||
"time_to_triage_hrs_max": <number or null>,
|
||||
"time_to_close_hrs_avg": <number or null>,
|
||||
"time_to_close_hrs_max": <number or null>,
|
||||
"time_triaged_closed_hrs_avg": <number or null>,
|
||||
"time_triaged_closed_hrs_max": <number or null>
|
||||
}
|
||||
},
|
||||
"open": [...],
|
||||
"closed": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL**: The output includes pre-calculated counts and health metrics:
|
||||
|
||||
- `summary`: Overall statistics across all components
|
||||
- `summary.total`: Total number of regressions
|
||||
- `summary.triaged`: Total number of regressions triaged (open + closed)
|
||||
- **`summary.triage_percentage`**: Percentage of all regressions that have been triaged (KEY HEALTH METRIC)
|
||||
- **`summary.time_to_triage_hrs_avg`**: Overall average hours to triage (combining open and closed, KEY HEALTH METRIC)
|
||||
- `summary.time_to_triage_hrs_max`: Overall maximum hours to triage
|
||||
- **`summary.time_to_close_hrs_avg`**: Overall average hours to close regressions (closed only, KEY HEALTH METRIC)
|
||||
- `summary.time_to_close_hrs_max`: Overall maximum hours to close regressions (closed only)
|
||||
- `summary.open.total`: Number of open regressions (where `closed` is null)
|
||||
- `summary.open.triaged`: Number of open regressions that have been triaged to a JIRA bug
|
||||
- `summary.open.triage_percentage`: Percentage of open regressions triaged
|
||||
- `summary.open.time_to_triage_hrs_avg`: Average hours from opened to first triage (open only)
|
||||
- `summary.open.time_to_triage_hrs_max`: Maximum hours from opened to first triage (open only)
|
||||
- `summary.open.open_hrs_avg`: Average hours that open regressions have been open (from opened to current time)
|
||||
- `summary.open.open_hrs_max`: Maximum hours that open regressions have been open (from opened to current time)
|
||||
- `summary.closed.total`: Number of closed regressions (where `closed` is not null)
|
||||
- `summary.closed.triaged`: Number of closed regressions that have been triaged to a JIRA bug
|
||||
- `summary.closed.triage_percentage`: Percentage of closed regressions triaged
|
||||
- `summary.closed.time_to_triage_hrs_avg`: Average hours from opened to first triage (closed only)
|
||||
- `summary.closed.time_to_triage_hrs_max`: Maximum hours from opened to first triage (closed only)
|
||||
- `summary.closed.time_to_close_hrs_avg`: Average hours from opened to closed timestamp (null if no valid data)
|
||||
- `summary.closed.time_to_close_hrs_max`: Maximum hours from opened to closed timestamp (null if no valid data)
|
||||
- `summary.closed.time_triaged_closed_hrs_avg`: Average hours from first triage to closed (null if no triaged closed regressions)
|
||||
- `summary.closed.time_triaged_closed_hrs_max`: Maximum hours from first triage to closed (null if no triaged closed regressions)
|
||||
- `components`: Dictionary mapping component names to objects containing:
|
||||
- `summary`: Per-component statistics (includes same fields as overall summary)
|
||||
- `open`: Array of open regression objects for that component
|
||||
- `closed`: Array of closed regression objects for that component
|
||||
|
||||
**Time to Triage Calculation**:
|
||||
|
||||
The `time_to_triage_hrs_avg` field is calculated as:
|
||||
|
||||
1. For each triaged regression, find the earliest `created_at` timestamp in the `triages` array
|
||||
2. Calculate the time difference between the regression's `opened` timestamp and the earliest triage timestamp
|
||||
3. Convert the difference to hours and round to the nearest hour
|
||||
4. Only include positive time differences (zero or negative values are skipped - these occur when triages are reused across regression instances)
|
||||
5. Average all valid time-to-triage values for open regressions separately from closed regressions
|
||||
6. Return `null` if no regressions have valid time-to-triage data in that category
|
||||
|
||||
**Time to Close Calculation**:
|
||||
|
||||
The `time_to_close_hrs_avg` and `time_to_close_hrs_max` fields (only for closed regressions) are calculated as:
|
||||
|
||||
1. For each closed regression, calculate the time difference between `opened` and `closed` timestamps
|
||||
2. Convert the difference to hours and round to the nearest hour
|
||||
3. Only include positive time differences (skip data inconsistencies)
|
||||
4. Calculate average and maximum of all valid time-to-close values
|
||||
5. Return `null` if no closed regressions have valid time data
|
||||
|
||||
**Open Duration Calculation**:
|
||||
|
||||
The `open_hrs_avg` and `open_hrs_max` fields (only for open regressions) are calculated as:
|
||||
|
||||
1. For each open regression, calculate the time difference between `opened` timestamp and current time
|
||||
2. Convert the difference to hours and round to the nearest hour
|
||||
3. Only include positive time differences
|
||||
4. Calculate average and maximum of all open duration values
|
||||
5. Return `null` if no open regressions have valid time data
|
||||
|
||||
**Time Triaged to Closed Calculation**:
|
||||
|
||||
The `time_triaged_closed_hrs_avg` and `time_triaged_closed_hrs_max` fields (only for triaged closed regressions) are calculated as:
|
||||
|
||||
1. For each closed regression that has been triaged, calculate the time difference between earliest `triages.created_at` timestamp and `closed` timestamp
|
||||
2. Convert the difference to hours and round to the nearest hour
|
||||
3. Only include positive time differences
|
||||
4. Calculate average and maximum of all triaged-to-closed values
|
||||
5. Return `null` if no triaged closed regressions have valid time data
|
||||
|
||||
**ALWAYS use these summary counts** rather than attempting to count the regression arrays yourself. This ensures accuracy even when the output is truncated due to size.
|
||||
|
||||
The script automatically simplifies and optimizes the response:
|
||||
|
||||
**Time field simplification** (`closed` and `last_failure`):
|
||||
|
||||
- Original API format: `{"Time": "2025-09-27T12:04:24.966914Z", "Valid": true}`
|
||||
- Simplified format: `"closed": "2025-09-27T12:04:24.966914Z"` (if Valid is true)
|
||||
- Or: `"closed": null` (if Valid is false)
|
||||
- Same applies to `last_failure` field
|
||||
|
||||
**Field removal for response size optimization**:
|
||||
|
||||
- `links`: Removed from each regression (reduces response size significantly)
|
||||
- `test_id`: Removed from each regression (large field, can be reconstructed from test_name if needed)
|
||||
|
||||
**Date filtering (optional)**:
|
||||
|
||||
- Use `--start` and `--end` parameters to filter regressions to a specific time window
|
||||
- `--start YYYY-MM-DD`: Excludes regressions that were closed before this date
|
||||
- `--end YYYY-MM-DD`: Excludes regressions that were opened after this date
|
||||
- Typical use case: Filter to the development window
|
||||
- `--start`: development_start date from get-release-dates skill (always applied)
|
||||
- `--end`: GA date from get-release-dates skill (only for GA'd releases)
|
||||
- For GA'd releases: Both start and end filtering applied
|
||||
- For in-development releases (null GA date): Only start filtering applied (no end date)
|
||||
- Benefits: Focuses analysis on regressions during active development, excluding:
|
||||
- Regressions closed before the release development started (not relevant)
|
||||
- Regressions opened after GA (post-release, often not monitored/triaged - GA'd releases only)
|
||||
|
||||
Parse this JSON output to extract relevant information for analysis.
|
||||
|
||||
### Step 5: Generate Analysis (Optional)
|
||||
|
||||
Based on the regression data:
|
||||
|
||||
1. **Use the summary counts** from the `summary` and `components.*.summary` objects (do NOT count the arrays)
|
||||
2. Identify most affected components using `components.*.summary.open.total`
|
||||
3. Compare with previous releases
|
||||
4. Analyze trends in open vs closed regressions per component
|
||||
5. Create visualizations if needed
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
1. **Network Errors**
|
||||
|
||||
- **Symptom**: `URLError` or connection timeout
|
||||
- **Solution**: Check network connectivity and firewall rules
|
||||
- **Retry**: The script has a 30-second timeout, consider retrying
|
||||
|
||||
2. **HTTP Errors**
|
||||
|
||||
- **Symptom**: HTTP 404, 500, etc.
|
||||
- **Solution**: Verify the API endpoint URL is correct
|
||||
- **Check**: Ensure the release parameter is valid
|
||||
|
||||
3. **Invalid Release**
|
||||
|
||||
- **Symptom**: Empty results or error response
|
||||
- **Solution**: Verify the release format (e.g., "4.17", not "v4.17")
|
||||
|
||||
4. **Invalid Boolean Value**
|
||||
- **Symptom**: `ValueError: Invalid boolean value`
|
||||
- **Solution**: Use only "true" or "false" for the --opened flag
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable verbose output by examining stderr:
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.17 2>&1 | tee debug.log
|
||||
```
|
||||
|
||||
## Script Arguments
|
||||
|
||||
### Required Arguments
|
||||
|
||||
- `--release`: Release version to query
|
||||
- Format: `"X.Y"` (e.g., "4.17", "4.16")
|
||||
- Must be a valid OpenShift release number
|
||||
|
||||
### Optional Arguments
|
||||
|
||||
- `--components`: Filter by component names
|
||||
- Values: Space-separated list of component names
|
||||
- Default: None (returns all components)
|
||||
- Case-insensitive matching
|
||||
- Examples: `--components Monitoring etcd "kube-apiserver"`
|
||||
- Filtering is performed after fetching data from the API
|
||||
|
||||
## Output Format
|
||||
|
||||
The script outputs JSON with summaries and regressions grouped by component:
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"total": 62,
|
||||
"triaged": 59,
|
||||
"triage_percentage": 95.2,
|
||||
"time_to_triage_hrs_avg": 68,
|
||||
"time_to_triage_hrs_max": 240,
|
||||
"time_to_close_hrs_avg": 168,
|
||||
"time_to_close_hrs_max": 480,
|
||||
"open": {
|
||||
"total": 2,
|
||||
"triaged": 1,
|
||||
"triage_percentage": 50.0,
|
||||
"time_to_triage_hrs_avg": 48,
|
||||
"time_to_triage_hrs_max": 48,
|
||||
"open_hrs_avg": 120,
|
||||
"open_hrs_max": 200
|
||||
},
|
||||
"closed": {
|
||||
"total": 60,
|
||||
"triaged": 58,
|
||||
"triage_percentage": 96.7,
|
||||
"time_to_triage_hrs_avg": 72,
|
||||
"time_to_triage_hrs_max": 240,
|
||||
"time_to_close_hrs_avg": 168,
|
||||
"time_to_close_hrs_max": 480,
|
||||
"time_triaged_closed_hrs_avg": 96,
|
||||
"time_triaged_closed_hrs_max": 240
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"Monitoring": {
|
||||
"summary": {
|
||||
"total": 15,
|
||||
"triaged": 13,
|
||||
"triage_percentage": 86.7,
|
||||
"time_to_triage_hrs_avg": 68,
|
||||
"time_to_triage_hrs_max": 180,
|
||||
"time_to_close_hrs_avg": 156,
|
||||
"time_to_close_hrs_max": 360,
|
||||
"open": {
|
||||
"total": 1,
|
||||
"triaged": 0,
|
||||
"triage_percentage": 0.0,
|
||||
"time_to_triage_hrs_avg": null,
|
||||
"time_to_triage_hrs_max": null,
|
||||
"open_hrs_avg": 72,
|
||||
"open_hrs_max": 72
|
||||
},
|
||||
"closed": {
|
||||
"total": 14,
|
||||
"triaged": 13,
|
||||
"triage_percentage": 92.9,
|
||||
"time_to_triage_hrs_avg": 68,
|
||||
"time_to_triage_hrs_max": 180,
|
||||
"time_to_close_hrs_avg": 156,
|
||||
"time_to_close_hrs_max": 360,
|
||||
"time_triaged_closed_hrs_avg": 88,
|
||||
"time_triaged_closed_hrs_max": 180
|
||||
}
|
||||
},
|
||||
"open": [
|
||||
{
|
||||
"id": 12894,
|
||||
"component": "Monitoring",
|
||||
"closed": null,
|
||||
...
|
||||
}
|
||||
],
|
||||
"closed": [
|
||||
{
|
||||
"id": 12893,
|
||||
"view": "4.21-main",
|
||||
"release": "4.21",
|
||||
"base_release": "4.18",
|
||||
"component": "Monitoring",
|
||||
"capability": "operator-conditions",
|
||||
"test_name": "...",
|
||||
"variants": [...],
|
||||
"opened": "2025-09-26T00:02:51.385944Z",
|
||||
"closed": "2025-09-27T12:04:24.966914Z",
|
||||
"triages": [],
|
||||
"last_failure": "2025-09-25T14:41:17Z",
|
||||
"max_failures": 9
|
||||
}
|
||||
]
|
||||
},
|
||||
"etcd": {
|
||||
"summary": {
|
||||
"total": 20,
|
||||
"triaged": 19,
|
||||
"triage_percentage": 95.0,
|
||||
"time_to_triage_hrs_avg": 84,
|
||||
"time_to_triage_hrs_max": 220,
|
||||
"time_to_close_hrs_avg": 192,
|
||||
"time_to_close_hrs_max": 500,
|
||||
"open": {
|
||||
"total": 0,
|
||||
"triaged": 0,
|
||||
"triage_percentage": 0.0,
|
||||
"time_to_triage_hrs_avg": null,
|
||||
"time_to_triage_hrs_max": null,
|
||||
"open_hrs_avg": null,
|
||||
"open_hrs_max": null
|
||||
},
|
||||
"closed": {
|
||||
"total": 20,
|
||||
"triaged": 19,
|
||||
"triage_percentage": 95.0,
|
||||
"time_to_triage_hrs_avg": 84,
|
||||
"time_to_triage_hrs_max": 220,
|
||||
"time_to_close_hrs_avg": 192,
|
||||
"time_to_close_hrs_max": 500,
|
||||
"time_triaged_closed_hrs_avg": 108,
|
||||
"time_triaged_closed_hrs_max": 280
|
||||
}
|
||||
},
|
||||
"open": [],
|
||||
"closed": [...]
|
||||
},
|
||||
"kube-apiserver": {
|
||||
"summary": {
|
||||
"total": 27,
|
||||
"triaged": 27,
|
||||
"triage_percentage": 100.0,
|
||||
"time_to_triage_hrs_avg": 58,
|
||||
"time_to_triage_hrs_max": 168,
|
||||
"time_to_close_hrs_avg": 144,
|
||||
"time_to_close_hrs_max": 400,
|
||||
"open": {
|
||||
"total": 1,
|
||||
"triaged": 1,
|
||||
"triage_percentage": 100.0,
|
||||
"time_to_triage_hrs_avg": 36,
|
||||
"time_to_triage_hrs_max": 36,
|
||||
"open_hrs_avg": 96,
|
||||
"open_hrs_max": 96
|
||||
},
|
||||
"closed": {
|
||||
"total": 26,
|
||||
"triaged": 26,
|
||||
"triage_percentage": 100.0,
|
||||
"time_to_triage_hrs_avg": 60,
|
||||
"time_to_triage_hrs_max": 168,
|
||||
"time_to_close_hrs_avg": 144,
|
||||
"time_to_close_hrs_max": 400,
|
||||
"time_triaged_closed_hrs_avg": 84,
|
||||
"time_triaged_closed_hrs_max": 232
|
||||
}
|
||||
},
|
||||
"open": [...],
|
||||
"closed": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important - Summary Objects**:
|
||||
|
||||
- The `summary` object contains overall pre-calculated counts for accuracy
|
||||
- Each component in the `components` object has its own `summary` with per-component counts
|
||||
- The `components` object maps component names (sorted alphabetically) to objects containing:
|
||||
- `summary`: Statistics for this component (total, open, closed)
|
||||
- `open`: Array of open regression objects (where `closed` is null)
|
||||
- `closed`: Array of closed regression objects (where `closed` has a timestamp)
|
||||
- **ALWAYS use the `summary` and `components.*.summary` fields** for counts (including `total`, `open.total`, `open.triaged`, `closed.total`, `closed.triaged`)
|
||||
- Do NOT attempt to count the `components.*.open` or `components.*.closed` arrays yourself
|
||||
|
||||
**Note**: Time fields are simplified from the API response:
|
||||
|
||||
- `closed`: If the regression is closed: `"closed": "2025-09-27T12:04:24.966914Z"` (timestamp string), otherwise `null`
|
||||
- `last_failure`: If valid: `"last_failure": "2025-09-25T14:41:17Z"` (timestamp string), otherwise `null`
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: List All Regressions
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.17
|
||||
```
|
||||
|
||||
**Expected Output**: JSON containing all regressions for release 4.17
|
||||
|
||||
### Example 2: Filter by Component
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.21 \
|
||||
--components Monitoring etcd
|
||||
```
|
||||
|
||||
**Expected Output**: JSON containing regressions for only Monitoring and etcd components in release 4.21
|
||||
|
||||
### Example 3: Filter by Single Component
|
||||
|
||||
```bash
|
||||
python3 plugins/component-health/skills/list-regressions/list_regressions.py \
|
||||
--release 4.21 \
|
||||
--components "kube-apiserver"
|
||||
```
|
||||
|
||||
**Expected Output**: JSON containing regressions for the kube-apiserver component in release 4.21
|
||||
|
||||
## Customization
|
||||
|
||||
### Updating the API Endpoint
|
||||
|
||||
The script includes a placeholder API endpoint. Update it in `list_regressions.py`:
|
||||
|
||||
```python
|
||||
# Current placeholder
|
||||
base_url = f"https://component-health-api.example.com/api/v1/regressions"
|
||||
|
||||
# Update to actual endpoint
|
||||
base_url = f"https://actual-api.example.com/api/v1/regressions"
|
||||
```
|
||||
|
||||
### Adding Custom Filters
|
||||
|
||||
To add additional query parameters, modify the `fetch_regressions` function:
|
||||
|
||||
```python
|
||||
def fetch_regressions(release: str, opened: Optional[bool] = None,
|
||||
component: Optional[str] = None) -> dict:
|
||||
params = [f"release={release}"]
|
||||
if opened is not None:
|
||||
params.append(f"opened={'true' if opened else 'false'}")
|
||||
if component is not None:
|
||||
params.append(f"component={component}")
|
||||
# ... rest of function
|
||||
```
|
||||
|
||||
## Integration with Commands
|
||||
|
||||
This skill is designed to be used by the `/component-health:analyze-regressions` command, but can also be invoked directly by other commands or scripts that need regression data.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- Component health analysis
|
||||
- Release comparison
|
||||
- Regression tracking
|
||||
- Quality metrics reporting
|
||||
|
||||
## Notes
|
||||
|
||||
- The script uses Python's built-in `urllib` module (no external dependencies)
|
||||
- Output is always JSON format for easy parsing
|
||||
- Diagnostic messages are written to stderr, data to stdout
|
||||
- The script has a 30-second timeout for HTTP requests
|
||||
670
skills/list-regressions/list_regressions.py
Executable file
670
skills/list-regressions/list_regressions.py
Executable file
@@ -0,0 +1,670 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to fetch regression data for OpenShift components.
|
||||
|
||||
Usage:
|
||||
python3 list_regressions.py --release <release> [--components comp1 comp2 ...] [--short]
|
||||
|
||||
Example:
|
||||
python3 list_regressions.py --release 4.17
|
||||
python3 list_regressions.py --release 4.21 --components Monitoring etcd
|
||||
python3 list_regressions.py --release 4.21 --short
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def calculate_hours_between(start_timestamp: str, end_timestamp: str) -> int:
|
||||
"""
|
||||
Calculate the number of hours between two timestamps, rounded to the nearest hour.
|
||||
|
||||
Args:
|
||||
start_timestamp: ISO format timestamp string (e.g., "2025-09-26T00:02:51.385944Z")
|
||||
end_timestamp: ISO format timestamp string (e.g., "2025-09-27T12:04:24.966914Z")
|
||||
|
||||
Returns:
|
||||
Number of hours between the timestamps, rounded to the nearest hour
|
||||
|
||||
Raises:
|
||||
ValueError: If timestamp parsing fails
|
||||
"""
|
||||
start_time = datetime.fromisoformat(start_timestamp.replace('Z', '+00:00'))
|
||||
end_time = datetime.fromisoformat(end_timestamp.replace('Z', '+00:00'))
|
||||
|
||||
time_diff = end_time - start_time
|
||||
return round(time_diff.total_seconds() / 3600)
|
||||
|
||||
|
||||
def fetch_regressions(release: str) -> dict:
|
||||
"""
|
||||
Fetch regression data from the component health API.
|
||||
|
||||
Args:
|
||||
release: The release version (e.g., "4.17", "4.16")
|
||||
|
||||
Returns:
|
||||
Dictionary containing the regression data
|
||||
|
||||
Raises:
|
||||
urllib.error.URLError: If the request fails
|
||||
"""
|
||||
# Construct the base URL
|
||||
base_url = f"https://sippy.dptools.openshift.org/api/component_readiness/regressions"
|
||||
|
||||
# Build query parameters
|
||||
params = [f"release={release}"]
|
||||
|
||||
url = f"{base_url}?{'&'.join(params)}"
|
||||
|
||||
print(f"Fetching regressions from: {url}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
if response.status == 200:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
return data
|
||||
else:
|
||||
raise Exception(f"HTTP {response.status}: {response.reason}")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
|
||||
raise
|
||||
except urllib.error.URLError as e:
|
||||
print(f"URL Error: {e.reason}", file=sys.stderr)
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
|
||||
def filter_by_components(data: list, components: list = None) -> list:
|
||||
"""
|
||||
Filter regression data by component names.
|
||||
|
||||
Args:
|
||||
data: List of regression dictionaries
|
||||
components: Optional list of component names to filter by
|
||||
|
||||
Returns:
|
||||
Filtered list of regressions matching the specified components
|
||||
"""
|
||||
# Always filter out regressions with empty component names
|
||||
# These are legacy prior to a code change to ensure it is always set.
|
||||
filtered = [
|
||||
regression for regression in data
|
||||
if regression.get('component', '') != ''
|
||||
]
|
||||
|
||||
# If no specific components requested, return all non-empty components
|
||||
if not components:
|
||||
return filtered
|
||||
|
||||
# Convert components to lowercase for case-insensitive comparison
|
||||
components_lower = [c.lower() for c in components]
|
||||
|
||||
# Further filter by specified components
|
||||
filtered = [
|
||||
regression for regression in filtered
|
||||
if regression.get('component', '').lower() in components_lower
|
||||
]
|
||||
|
||||
print(f"Filtered to {len(filtered)} regressions for components: {', '.join(components)}",
|
||||
file=sys.stderr)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def simplify_time_fields(data: list) -> list:
|
||||
"""
|
||||
Simplify time fields in regression data.
|
||||
|
||||
Converts time fields from a nested structure like:
|
||||
{"Time": "2025-09-27T12:04:24.966914Z", "Valid": true}
|
||||
to either:
|
||||
- The timestamp string if Valid is true
|
||||
- null if Valid is false
|
||||
|
||||
This applies to fields: 'closed', 'last_failure'
|
||||
|
||||
Args:
|
||||
data: List of regression dictionaries
|
||||
|
||||
Returns:
|
||||
List of regressions with simplified time fields
|
||||
"""
|
||||
time_fields = ['closed', 'last_failure']
|
||||
|
||||
for regression in data:
|
||||
for field in time_fields:
|
||||
if field in regression:
|
||||
value = regression[field]
|
||||
# Check if the field is a dict with Valid and Time fields
|
||||
if isinstance(value, dict):
|
||||
if value.get('Valid') is True:
|
||||
# Replace with just the timestamp string
|
||||
regression[field] = value.get('Time')
|
||||
else:
|
||||
# Replace with null if not valid
|
||||
regression[field] = None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def filter_by_date_range(regressions: list, start_date: str = None, end_date: str = None) -> list:
|
||||
"""
|
||||
Filter regressions by date range.
|
||||
|
||||
Args:
|
||||
regressions: List of regression dictionaries
|
||||
start_date: Start date in YYYY-MM-DD format. Filters out regressions closed before this date.
|
||||
end_date: End date in YYYY-MM-DD format. Filters out regressions opened after this date.
|
||||
|
||||
Returns:
|
||||
Filtered list of regressions
|
||||
|
||||
Note:
|
||||
- If start_date is provided: excludes regressions that were closed before start_date
|
||||
- If end_date is provided: excludes regressions that were opened after end_date
|
||||
- This allows filtering to a development window (e.g., from development_start to GA)
|
||||
"""
|
||||
if not start_date and not end_date:
|
||||
return regressions
|
||||
|
||||
filtered = []
|
||||
|
||||
for regression in regressions:
|
||||
# Skip if opened after end_date
|
||||
if end_date and regression.get('opened'):
|
||||
opened_date = regression['opened'].split('T')[0] # Extract YYYY-MM-DD
|
||||
if opened_date > end_date:
|
||||
continue
|
||||
|
||||
# Skip if closed before start_date
|
||||
if start_date and regression.get('closed'):
|
||||
closed_date = regression['closed'].split('T')[0] # Extract YYYY-MM-DD
|
||||
if closed_date < start_date:
|
||||
continue
|
||||
|
||||
filtered.append(regression)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def remove_unnecessary_fields(regressions: list) -> list:
|
||||
"""
|
||||
Remove unnecessary fields from regressions to reduce response size.
|
||||
|
||||
Removes 'links' and 'test_id' fields from each regression object.
|
||||
|
||||
Args:
|
||||
regressions: List of regression dictionaries
|
||||
|
||||
Returns:
|
||||
List of regression dictionaries with unnecessary fields removed
|
||||
"""
|
||||
for regression in regressions:
|
||||
# Remove links and test_id to reduce response size
|
||||
regression.pop('links', None)
|
||||
regression.pop('test_id', None)
|
||||
|
||||
return regressions
|
||||
|
||||
|
||||
def exclude_suspected_infra_regressions(regressions: list) -> tuple[list, int]:
|
||||
"""
|
||||
Filter out suspected infrastructure-related mass regressions.
|
||||
|
||||
This is an imprecise attempt to filter out mass regressions caused by infrastructure
|
||||
issues which the TRT handles via a separate mechanism. These
|
||||
mass incidents typically result in many short-lived regressions being opened and
|
||||
closed on the same day.
|
||||
|
||||
Algorithm:
|
||||
1. First pass: Count how many short-lived regressions (closed within 96 hours of opening)
|
||||
were closed on each date.
|
||||
2. Second pass: Filter out regressions that:
|
||||
- Were closed within 96 hours of being opened, AND
|
||||
- Were closed on a date where >50 short-lived regressions were closed
|
||||
|
||||
Args:
|
||||
regressions: List of regression dictionaries
|
||||
|
||||
Returns:
|
||||
Tuple of (filtered_regressions, count_of_filtered_regressions)
|
||||
"""
|
||||
# First pass: Track count of short-lived regressions closed on each date
|
||||
short_lived_closures_by_date = {}
|
||||
|
||||
for regression in regressions:
|
||||
opened = regression.get('opened')
|
||||
closed = regression.get('closed')
|
||||
|
||||
# Skip if not closed or missing opened timestamp
|
||||
if not closed or not opened:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Calculate how long the regression was open
|
||||
hours_open = calculate_hours_between(opened, closed)
|
||||
|
||||
# If closed within 96 hours, increment counter for the closed date
|
||||
if hours_open <= 96:
|
||||
closed_date = closed.split('T')[0] # Extract YYYY-MM-DD
|
||||
short_lived_closures_by_date[closed_date] = short_lived_closures_by_date.get(closed_date, 0) + 1
|
||||
except (ValueError, KeyError, TypeError):
|
||||
# Skip if timestamp parsing fails
|
||||
continue
|
||||
|
||||
# Second pass: Filter out suspected infra regressions
|
||||
filtered_regressions = []
|
||||
filtered_count = 0
|
||||
|
||||
for regression in regressions:
|
||||
opened = regression.get('opened')
|
||||
closed = regression.get('closed')
|
||||
|
||||
# Keep open regressions
|
||||
if not closed or not opened:
|
||||
filtered_regressions.append(regression)
|
||||
continue
|
||||
|
||||
try:
|
||||
# Calculate how long the regression was open
|
||||
hours_open = calculate_hours_between(opened, closed)
|
||||
closed_date = closed.split('T')[0] # Extract YYYY-MM-DD
|
||||
|
||||
# Filter out if:
|
||||
# 1. Was closed within 96 hours, AND
|
||||
# 2. More than 50 short-lived regressions were closed on that date
|
||||
if hours_open <= 96 and short_lived_closures_by_date.get(closed_date, 0) > 50:
|
||||
filtered_count += 1
|
||||
continue
|
||||
|
||||
# Keep this regression
|
||||
filtered_regressions.append(regression)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
# If timestamp parsing fails, keep the regression
|
||||
filtered_regressions.append(regression)
|
||||
|
||||
return filtered_regressions, filtered_count
|
||||
|
||||
|
||||
def group_by_component(data: list) -> dict:
|
||||
"""
|
||||
Group regressions by component name and split into open/closed.
|
||||
|
||||
Args:
|
||||
data: List of regression dictionaries
|
||||
|
||||
Returns:
|
||||
Dictionary mapping component names to objects containing open and closed regression lists
|
||||
"""
|
||||
components = {}
|
||||
|
||||
for regression in data:
|
||||
component = regression.get('component', 'Unknown')
|
||||
if component not in components:
|
||||
components[component] = {
|
||||
"open": [],
|
||||
"closed": []
|
||||
}
|
||||
|
||||
# Split based on whether closed field is null
|
||||
if regression.get('closed') is None:
|
||||
components[component]["open"].append(regression)
|
||||
else:
|
||||
components[component]["closed"].append(regression)
|
||||
|
||||
# Sort component names for consistent output
|
||||
return dict(sorted(components.items()))
|
||||
|
||||
|
||||
def calculate_summary(regressions: list, filtered_suspected_infra: int = 0) -> dict:
|
||||
"""
|
||||
Calculate summary statistics for a list of regressions.
|
||||
|
||||
Args:
|
||||
regressions: List of regression dictionaries
|
||||
filtered_suspected_infra: Count of regressions filtered out as suspected infrastructure issues
|
||||
|
||||
Returns:
|
||||
Dictionary containing summary statistics with nested open/closed totals, triaged counts,
|
||||
and average time to triage
|
||||
"""
|
||||
total = 0
|
||||
open_total = 0
|
||||
open_triaged = 0
|
||||
open_triage_times = []
|
||||
open_times = []
|
||||
closed_total = 0
|
||||
closed_triaged = 0
|
||||
closed_triage_times = []
|
||||
closed_times = []
|
||||
triaged_to_closed_times = []
|
||||
|
||||
# Get current time for calculating open duration
|
||||
current_time = datetime.now(timezone.utc)
|
||||
current_time_str = current_time.isoformat().replace('+00:00', 'Z')
|
||||
|
||||
# Single pass through all regressions
|
||||
for regression in regressions:
|
||||
total += 1
|
||||
triages = regression.get('triages', [])
|
||||
is_triaged = bool(triages)
|
||||
|
||||
# Calculate time to triage if regression is triaged
|
||||
time_to_triage_hrs = None
|
||||
if is_triaged and regression.get('opened'):
|
||||
try:
|
||||
# Find earliest triage timestamp
|
||||
earliest_triage_time = min(
|
||||
t['created_at'] for t in triages if t.get('created_at')
|
||||
)
|
||||
|
||||
# Calculate difference in hours
|
||||
time_to_triage_hrs = calculate_hours_between(
|
||||
regression['opened'],
|
||||
earliest_triage_time
|
||||
)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
# Skip if timestamp parsing fails
|
||||
pass
|
||||
|
||||
# It is common for a triage to be reused as new regressions appear, which makes this a very tricky case to calculate time to triage.
|
||||
# If you triaged a first round of regressions, then added more 24 hours later, we don't actually know when you triaged them in the db.
|
||||
# Treating them as if they were immediately triaged would skew results.
|
||||
# Best we can do is ignore these from consideration. They will count as if they got triaged, but we have no idea what to do with the time to triage.
|
||||
if regression.get('closed') is None:
|
||||
# Open regression
|
||||
open_total += 1
|
||||
if is_triaged:
|
||||
open_triaged += 1
|
||||
if time_to_triage_hrs is not None and time_to_triage_hrs > 0:
|
||||
open_triage_times.append(time_to_triage_hrs)
|
||||
|
||||
# Calculate how long regression has been open
|
||||
if regression.get('opened'):
|
||||
try:
|
||||
time_open_hrs = calculate_hours_between(
|
||||
regression['opened'],
|
||||
current_time_str
|
||||
)
|
||||
# Only include positive time differences
|
||||
if time_open_hrs > 0:
|
||||
open_times.append(time_open_hrs)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
# Skip if timestamp parsing fails
|
||||
pass
|
||||
else:
|
||||
# Closed regression
|
||||
closed_total += 1
|
||||
if is_triaged:
|
||||
closed_triaged += 1
|
||||
if time_to_triage_hrs is not None and time_to_triage_hrs > 0:
|
||||
closed_triage_times.append(time_to_triage_hrs)
|
||||
|
||||
# Calculate time from triage to closed
|
||||
if regression.get('closed') and triages:
|
||||
try:
|
||||
earliest_triage_time = min(
|
||||
t['created_at'] for t in triages if t.get('created_at')
|
||||
)
|
||||
time_triaged_to_closed_hrs = calculate_hours_between(
|
||||
earliest_triage_time,
|
||||
regression['closed']
|
||||
)
|
||||
# Only include positive time differences:
|
||||
if time_triaged_to_closed_hrs > 0:
|
||||
triaged_to_closed_times.append(time_triaged_to_closed_hrs)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
# Skip if timestamp parsing fails
|
||||
pass
|
||||
|
||||
# Calculate time to close
|
||||
if regression.get('opened') and regression.get('closed'):
|
||||
try:
|
||||
time_to_close_hrs = calculate_hours_between(
|
||||
regression['opened'],
|
||||
regression['closed']
|
||||
)
|
||||
# Only include positive time differences
|
||||
if time_to_close_hrs > 0:
|
||||
closed_times.append(time_to_close_hrs)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
# Skip if timestamp parsing fails
|
||||
pass
|
||||
|
||||
# Calculate averages and maximums
|
||||
open_avg_triage_time = round(sum(open_triage_times) / len(open_triage_times)) if open_triage_times else None
|
||||
open_max_triage_time = max(open_triage_times) if open_triage_times else None
|
||||
open_avg_time = round(sum(open_times) / len(open_times)) if open_times else None
|
||||
open_max_time = max(open_times) if open_times else None
|
||||
closed_avg_triage_time = round(sum(closed_triage_times) / len(closed_triage_times)) if closed_triage_times else None
|
||||
closed_max_triage_time = max(closed_triage_times) if closed_triage_times else None
|
||||
closed_avg_time = round(sum(closed_times) / len(closed_times)) if closed_times else None
|
||||
closed_max_time = max(closed_times) if closed_times else None
|
||||
triaged_to_closed_avg_time = round(sum(triaged_to_closed_times) / len(triaged_to_closed_times)) if triaged_to_closed_times else None
|
||||
triaged_to_closed_max_time = max(triaged_to_closed_times) if triaged_to_closed_times else None
|
||||
|
||||
# Calculate triage percentages
|
||||
total_triaged = open_triaged + closed_triaged
|
||||
triage_percentage = round((total_triaged / total * 100), 1) if total > 0 else 0
|
||||
open_triage_percentage = round((open_triaged / open_total * 100), 1) if open_total > 0 else 0
|
||||
closed_triage_percentage = round((closed_triaged / closed_total * 100), 1) if closed_total > 0 else 0
|
||||
|
||||
# Calculate overall time to triage (combining open and closed)
|
||||
all_triage_times = open_triage_times + closed_triage_times
|
||||
overall_avg_triage_time = round(sum(all_triage_times) / len(all_triage_times)) if all_triage_times else None
|
||||
overall_max_triage_time = max(all_triage_times) if all_triage_times else None
|
||||
|
||||
# Time to close is only for closed regressions (already calculated in closed_avg_time/closed_max_time)
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"triaged": total_triaged,
|
||||
"triage_percentage": triage_percentage,
|
||||
"filtered_suspected_infra_regressions": filtered_suspected_infra,
|
||||
"time_to_triage_hrs_avg": overall_avg_triage_time,
|
||||
"time_to_triage_hrs_max": overall_max_triage_time,
|
||||
"time_to_close_hrs_avg": closed_avg_time,
|
||||
"time_to_close_hrs_max": closed_max_time,
|
||||
"open": {
|
||||
"total": open_total,
|
||||
"triaged": open_triaged,
|
||||
"triage_percentage": open_triage_percentage,
|
||||
"time_to_triage_hrs_avg": open_avg_triage_time,
|
||||
"time_to_triage_hrs_max": open_max_triage_time,
|
||||
"open_hrs_avg": open_avg_time,
|
||||
"open_hrs_max": open_max_time
|
||||
},
|
||||
"closed": {
|
||||
"total": closed_total,
|
||||
"triaged": closed_triaged,
|
||||
"triage_percentage": closed_triage_percentage,
|
||||
"time_to_triage_hrs_avg": closed_avg_triage_time,
|
||||
"time_to_triage_hrs_max": closed_max_triage_time,
|
||||
"time_to_close_hrs_avg": closed_avg_time,
|
||||
"time_to_close_hrs_max": closed_max_time,
|
||||
"time_triaged_closed_hrs_avg": triaged_to_closed_avg_time,
|
||||
"time_triaged_closed_hrs_max": triaged_to_closed_max_time
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def add_component_summaries(components: dict) -> dict:
|
||||
"""
|
||||
Add summary statistics to each component object.
|
||||
|
||||
Args:
|
||||
components: Dictionary mapping component names to objects containing open and closed regression lists
|
||||
|
||||
Returns:
|
||||
Dictionary with summaries added to each component
|
||||
"""
|
||||
for component, component_data in components.items():
|
||||
# Combine open and closed to get all regressions for this component
|
||||
all_regressions = component_data["open"] + component_data["closed"]
|
||||
component_data["summary"] = calculate_summary(all_regressions)
|
||||
|
||||
return components
|
||||
|
||||
|
||||
def format_output(data: dict) -> str:
|
||||
"""
|
||||
Format the regression data for output.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing regression data with keys:
|
||||
- 'summary': Overall statistics (total, open, closed)
|
||||
- 'components': Dictionary mapping component names to objects with:
|
||||
- 'summary': Per-component statistics
|
||||
- 'open': List of open regression objects
|
||||
- 'closed': List of closed regression objects
|
||||
|
||||
Returns:
|
||||
Formatted JSON string output
|
||||
"""
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Fetch regression data for OpenShift components',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# List all regressions for release 4.17
|
||||
%(prog)s --release 4.17
|
||||
|
||||
# Filter by specific components
|
||||
%(prog)s --release 4.21 --components Monitoring "kube-apiserver"
|
||||
|
||||
# Filter by multiple components
|
||||
%(prog)s --release 4.21 --components Monitoring etcd "kube-apiserver"
|
||||
|
||||
# Short output mode (summaries only, no regression data)
|
||||
%(prog)s --release 4.17 --short
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--release',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Release version (e.g., "4.17", "4.16")'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--components',
|
||||
type=str,
|
||||
nargs='+',
|
||||
default=None,
|
||||
help='Filter by component names (space-separated list, case-insensitive)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--start',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Start date for filtering (YYYY-MM-DD format, e.g., "2022-03-10"). Filters out regressions closed before this date.'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--end',
|
||||
type=str,
|
||||
default=None,
|
||||
help='End date for filtering (YYYY-MM-DD format, e.g., "2022-08-10"). Filters out regressions opened after this date.'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--short',
|
||||
action='store_true',
|
||||
help='Short output mode: exclude regression data, only include summaries'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
|
||||
try:
|
||||
# Fetch regressions
|
||||
regressions = fetch_regressions(args.release)
|
||||
|
||||
# Filter by components (always called to remove empty component names)
|
||||
if isinstance(regressions, list):
|
||||
regressions = filter_by_components(regressions, args.components)
|
||||
|
||||
# Simplify time field structures (closed, last_failure)
|
||||
if isinstance(regressions, list):
|
||||
regressions = simplify_time_fields(regressions)
|
||||
|
||||
# Filter by date range (to focus on development window)
|
||||
if isinstance(regressions, list):
|
||||
regressions = filter_by_date_range(regressions, args.start, args.end)
|
||||
|
||||
# Remove unnecessary fields to reduce response size
|
||||
if isinstance(regressions, list):
|
||||
regressions = remove_unnecessary_fields(regressions)
|
||||
|
||||
# Filter out suspected infrastructure regressions
|
||||
filtered_infra_count = 0
|
||||
if isinstance(regressions, list):
|
||||
regressions, filtered_infra_count = exclude_suspected_infra_regressions(regressions)
|
||||
print(f"Filtered out {filtered_infra_count} suspected infrastructure regressions",
|
||||
file=sys.stderr)
|
||||
|
||||
# Group regressions by component
|
||||
if isinstance(regressions, list):
|
||||
components = group_by_component(regressions)
|
||||
else:
|
||||
components = {}
|
||||
|
||||
# Add summaries to each component
|
||||
if isinstance(components, dict):
|
||||
components = add_component_summaries(components)
|
||||
|
||||
# Calculate overall summary statistics from all regressions
|
||||
all_regressions = []
|
||||
for comp_data in components.values():
|
||||
all_regressions.extend(comp_data["open"])
|
||||
all_regressions.extend(comp_data["closed"])
|
||||
|
||||
overall_summary = calculate_summary(all_regressions, filtered_infra_count)
|
||||
|
||||
# Construct output with summary and components
|
||||
# If --short flag is specified, remove regression data from components
|
||||
if args.short:
|
||||
# Create a copy of components with only summaries
|
||||
components_short = {}
|
||||
for component_name, component_data in components.items():
|
||||
components_short[component_name] = {
|
||||
"summary": component_data["summary"]
|
||||
}
|
||||
output_data = {
|
||||
"summary": overall_summary,
|
||||
"components": components_short
|
||||
}
|
||||
else:
|
||||
output_data = {
|
||||
"summary": overall_summary,
|
||||
"components": components
|
||||
}
|
||||
|
||||
# Format and print output
|
||||
output = format_output(output_data)
|
||||
print(output)
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch regressions: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user