Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions .github/workflows/generate_matrix_page.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ jobs:
steps:
- name: Set dynamic env vars
run: |
# GPU Operator dashboard paths
echo "DASHBOARD_DATA_FILEPATH=${DASHBOARD_OUTPUT_DIR}/gpu_operator_matrix.json" >> "$GITHUB_ENV"
echo "DASHBOARD_HTML_FILEPATH=${DASHBOARD_OUTPUT_DIR}/gpu_operator_matrix.html" >> "$GITHUB_ENV"
# Network Operator dashboard paths
echo "NNO_DASHBOARD_DATA_FILEPATH=${DASHBOARD_OUTPUT_DIR}/network_operator_matrix.json" >> "$GITHUB_ENV"
echo "NNO_DASHBOARD_HTML_FILEPATH=${DASHBOARD_OUTPUT_DIR}/network_operator_matrix.html" >> "$GITHUB_ENV"
echo "GH_PAGES_BRANCH=${{ github.event.inputs.gh_pages_branch || 'gh-pages' }}" >> "$GITHUB_ENV"
env:
DASHBOARD_OUTPUT_DIR: ${{ env.DASHBOARD_OUTPUT_DIR }}
Expand Down Expand Up @@ -67,27 +71,62 @@ jobs:
- name: Install Dependencies
run: |
pip install -r workflows/gpu_operator_dashboard/requirements.txt
pip install -r workflows/nno_dashboard/requirements.txt

- name: Fetch CI Data
run: |
echo "Processing PR: ${{ steps.determine_pr.outputs.PR_NUMBER }}"
# GPU Operator
python -m workflows.gpu_operator_dashboard.fetch_ci_data \
--pr_number "${{ steps.determine_pr.outputs.PR_NUMBER }}" \
--baseline_data_filepath "${{ env.DASHBOARD_DATA_FILEPATH }}" \
--merged_data_filepath "${{ env.DASHBOARD_DATA_FILEPATH }}"
# Network Operator
python -m workflows.nno_dashboard.fetch_ci_data \
--pr_number "${{ steps.determine_pr.outputs.PR_NUMBER }}" \
--baseline_data_filepath "${{ env.NNO_DASHBOARD_DATA_FILEPATH }}" \
--merged_data_filepath "${{ env.NNO_DASHBOARD_DATA_FILEPATH }}"


- name: Generate HTML Dashboard (only if JSON changed)
run: |
cd "${{ env.DASHBOARD_OUTPUT_DIR }}"

# Check if GPU Operator JSON changed
GPU_CHANGED=false
if [[ ${{ github.event_name }} == "pull_request_target" ]] && git diff --exit-code gpu_operator_matrix.json; then
echo "no changes"
echo "GPU Operator: no changes"
else
echo "GPU Operator: changes detected"
GPU_CHANGED=true
fi

# Check if Network Operator JSON changed
NNO_CHANGED=false
if [[ ${{ github.event_name }} == "pull_request_target" ]] && git diff --exit-code network_operator_matrix.json; then
echo "Network Operator: no changes"
else
cd "${{ github.workspace }}"
echo "Network Operator: changes detected"
NNO_CHANGED=true
fi

cd "${{ github.workspace }}"

# Generate GPU Operator dashboard if changed
if [ "$GPU_CHANGED" = true ]; then
echo "Generating GPU Operator dashboard..."
python -m workflows.gpu_operator_dashboard.generate_ci_dashboard \
--dashboard_data_filepath "${{ env.DASHBOARD_DATA_FILEPATH }}" \
--dashboard_html_filepath "${{ env.DASHBOARD_HTML_FILEPATH }}"
fi

# Generate Network Operator dashboard if changed
if [ "$NNO_CHANGED" = true ]; then
echo "Generating Network Operator dashboard..."
python -m workflows.nno_dashboard.generate_ci_dashboard \
--dashboard_data_filepath "${{ env.NNO_DASHBOARD_DATA_FILEPATH }}" \
--dashboard_html_filepath "${{ env.NNO_DASHBOARD_HTML_FILEPATH }}"
fi

- name: Deploy HTML to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
__pycache__/
venv/
*.pyc
.DS_Store
.vscode/
3 changes: 2 additions & 1 deletion workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ This directory contains multiple workflows for automating various aspects of the

- [gpu_operator_versions/](./gpu_operator_versions/) — Automation for updating versions and triggering CI jobs
- [gpu_operator_dashboard/](./gpu_operator_dashboard/) — CI dashboard generation for NVIDIA GPU Operator test results
- [nno_dashboard/](./nno_dashboard/) — CI dashboard generation for NVIDIA Network Operator test results
- [microshift_dashboard/](./microshift_dashboard/) — MicroShift NVIDIA Device Plugin testing dashboard
- Shared modules: [utils.py](./utils.py), [templates.py](./templates.py)
- [common/](./common/) — Shared utilities: logging, templates, GCS access, HTML builders, data structures

See the individual README files in each subdirectory for detailed information.

Expand Down
94 changes: 94 additions & 0 deletions workflows/common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
Common utilities shared across NVIDIA CI workflows.
"""

from workflows.common.utils import get_logger, logger
from workflows.common.templates import load_template
from workflows.common.data_structures import (
TestResult,
OCP_FULL_VERSION,
OPERATOR_VERSION,
GPU_OPERATOR_VERSION,
STATUS_SUCCESS,
STATUS_FAILURE,
STATUS_ABORTED,
)
from workflows.common.gcs_utils import (
http_get_json,
fetch_gcs_file_content,
build_prow_job_url,
fetch_filtered_files,
build_job_history_url,
GCS_API_BASE_URL,
GCS_MAX_RESULTS_PER_REQUEST,
)
from workflows.common.html_builders import (
build_toc,
build_notes,
build_history_bar,
build_last_updated_footer,
sanitize_id,
)
from workflows.common.validation import (
is_valid_ocp_version,
has_valid_semantic_versions,
is_infrastructure_type,
)
from workflows.common.data_fetching import (
build_version_lookups,
build_finished_lookup,
extract_test_status,
extract_timestamp,
determine_repo_from_job_name,
convert_sets_to_lists_recursive,
merge_job_history_links,
int_or_none,
)

__all__ = [
# Utils
"get_logger",
"logger",
"load_template",

# Data structures
"TestResult",
"OCP_FULL_VERSION",
"OPERATOR_VERSION",
"GPU_OPERATOR_VERSION",
"STATUS_SUCCESS",
"STATUS_FAILURE",
"STATUS_ABORTED",

# GCS utilities
"http_get_json",
"fetch_gcs_file_content",
"build_prow_job_url",
"fetch_filtered_files",
"build_job_history_url",
"GCS_API_BASE_URL",
"GCS_MAX_RESULTS_PER_REQUEST",

# HTML builders
"build_toc",
"build_notes",
"build_history_bar",
"build_last_updated_footer",
"sanitize_id",

# Validation
"is_valid_ocp_version",
"has_valid_semantic_versions",
"is_infrastructure_type",

# Data fetching
"build_version_lookups",
"build_finished_lookup",
"extract_test_status",
"extract_timestamp",
"determine_repo_from_job_name",
"convert_sets_to_lists_recursive",
"merge_job_history_links",
"int_or_none",
]

177 changes: 177 additions & 0 deletions workflows/common/data_fetching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
Common data fetching patterns for CI dashboards.
Shared logic for building file lookups and processing builds.
"""

import json
from typing import Dict, Any, List, Tuple, Optional
from workflows.common.utils import logger
from workflows.common.gcs_utils import fetch_gcs_file_content


def build_version_lookups(
version_files_list: List[Tuple[str, List[Dict[str, Any]]]]
) -> Dict[str, Dict[str, str]]:
"""
Build lookup dictionaries for version files organized by build directory.

Args:
version_files_list: List of tuples (file_type, file_items)
e.g., [("ocp", ocp_files), ("operator", operator_files)]

Returns:
Dict mapping file_type to {build_dir: content}
e.g., {"ocp": {build_dir: "4.17.16"}, "operator": {build_dir: "25.4.0"}}
"""
version_lookups = {}

for file_type, file_items in version_files_list:
lookup = {}
for file_item in file_items:
path = file_item["name"]
build_dir = path.rsplit("/", 1)[0]
try:
content = fetch_gcs_file_content(path)
lookup[build_dir] = content.strip()
except Exception as e:
logger.warning(f"Failed to fetch {file_type} from {path}: {e}")
version_lookups[file_type] = lookup

return version_lookups


def build_finished_lookup(
finished_files: List[Dict[str, Any]]
) -> Dict[str, Dict[str, Any]]:
"""
Build lookup dictionary for finished.json files by build directory.

Args:
finished_files: List of finished.json file items from GCS

Returns:
Dict mapping build_dir to parsed finished.json content
"""
finished_lookup = {}

for finished_item in finished_files:
finished_path = finished_item["name"]
build_dir = finished_path.rsplit("/", 1)[0]
try:
content = fetch_gcs_file_content(finished_path)
finished_lookup[build_dir] = json.loads(content)
except Exception as e:
logger.warning(f"Failed to fetch/parse finished.json from {finished_path}: {e}")

return finished_lookup


def extract_test_status(
finished_json: Dict[str, Any],
status_success: str,
status_failure: str,
status_aborted: str
) -> str:
"""
Extract and normalize test status from finished.json.

Args:
finished_json: Parsed finished.json content
status_success: String constant for success status
status_failure: String constant for failure status
status_aborted: String constant for aborted status

Returns:
Normalized test status string
"""
result_str = finished_json.get("result", "UNKNOWN").upper()
if result_str in [status_success, status_failure, status_aborted]:
return result_str
return status_failure


def extract_timestamp(finished_json: Dict[str, Any]) -> int:
"""
Extract timestamp from finished.json.

Args:
finished_json: Parsed finished.json content

Returns:
Unix timestamp (defaults to 0 if not found)
"""
return finished_json.get("timestamp", 0)


def determine_repo_from_job_name(job_name: str) -> str:
"""
Determine repository from job name pattern.

Args:
job_name: Job name string

Returns:
Repository identifier ('openshift_release' or 'rh-ecosystem-edge_nvidia-ci')
"""
return "openshift_release" if job_name.startswith("rehearse-") else "rh-ecosystem-edge_nvidia-ci"


def convert_sets_to_lists_recursive(data: Any) -> Any:
"""
Recursively convert sets to sorted lists for JSON serialization.

Args:
data: Any data structure that may contain sets

Returns:
Data structure with sets converted to sorted lists
"""
if isinstance(data, set):
return sorted(list(data))
elif isinstance(data, dict):
return {k: convert_sets_to_lists_recursive(v) for k, v in data.items()}
elif isinstance(data, list):
return [convert_sets_to_lists_recursive(item) for item in data]
else:
return data


def merge_job_history_links(
new_links: Any,
existing_links: Any
) -> List[str]:
"""
Merge and deduplicate job history links.

Args:
new_links: New links (can be set or list)
existing_links: Existing links (can be set or list)

Returns:
Sorted list of unique links
"""
# Convert both to sets
new_set = set(new_links) if isinstance(new_links, (set, list)) else set()
existing_set = set(existing_links) if isinstance(existing_links, (set, list)) else set()

# Merge and return sorted list
all_links = new_set | existing_set
return sorted(list(all_links))


def int_or_none(value: Optional[str]) -> Optional[int]:
"""
Convert string to int or None for unlimited.

Args:
value: String value to convert

Returns:
Integer or None
"""
if value is None:
return None
if value.lower() in ('none', 'unlimited'):
return None
return int(value)

Loading