Files
TheRock/build_tools/github_actions/tests/workflow_utils.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

202 lines
6.2 KiB
Python

# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Shared helpers for workflow YAML tests."""
import re
from pathlib import Path
import yaml
WORKFLOWS_DIR = Path(__file__).resolve().parents[3] / ".github" / "workflows"
_MATRIX_REFERENCE_RE = re.compile(r"\bmatrix\.([A-Za-z_][A-Za-z0-9_]*)")
def load_workflow(path: Path) -> dict:
"""Loads a YAML workflow file from the given Path as a JSON dictionary."""
with open(path) as f:
return yaml.safe_load(f)
def get_workflow_job(workflow: dict, job_name: str) -> dict:
"""Returns a workflow job definition.
For a workflow with:
jobs:
build_wheels:
uses: ./.github/workflows/build_wheels.yml
with:
python_version: ${{ matrix.python_version }}
get_workflow_job(workflow, "build_wheels") returns the dictionary
containing the uses/with blocks for that job.
"""
jobs = workflow.get("jobs")
if not isinstance(jobs, dict):
raise KeyError("workflow has no jobs block")
job = jobs[job_name]
if not isinstance(job, dict):
raise KeyError(f"workflow job {job_name!r} is not a mapping")
return job
def get_matrix_references(value: object) -> set[str]:
"""Extracts top-level matrix keys referenced by a workflow YAML value.
For a workflow value with:
with:
python_version: ${{ matrix.python_version }}
package_url: >-
${{
format('{0}/{1}/index.html',
needs.build.outputs.package_find_links_url,
matrix.amdgpu_family)
}}
get_matrix_references(value) returns:
{"python_version", "amdgpu_family"}
Nested matrix objects like matrix.family_info.amdgpu_family return only the
top-level matrix key, {"family_info"}.
"""
if isinstance(value, str):
return set(_MATRIX_REFERENCE_RE.findall(value))
if isinstance(value, dict):
references = set()
for child_value in value.values():
references.update(get_matrix_references(child_value))
return references
if isinstance(value, list):
references = set()
for child_value in value:
references.update(get_matrix_references(child_value))
return references
return set()
def _get_workflow_dispatch_block(workflow: dict) -> dict | None:
"""Returns the workflow_dispatch block, or None."""
# PyYAML parses the unquoted YAML key `on:` as boolean True.
on_block = workflow.get("on") or workflow.get(True)
if not isinstance(on_block, dict):
return None
dispatch = on_block.get("workflow_dispatch")
if not isinstance(dispatch, dict):
return None
return dispatch
def _get_dispatch_inputs(workflow: dict) -> dict:
"""Returns the workflow_dispatch inputs dict, or empty dict."""
dispatch = _get_workflow_dispatch_block(workflow)
if dispatch is None:
return {}
inputs = dispatch.get("inputs")
if not isinstance(inputs, dict):
return {}
return inputs
def get_workflow_dispatch_inputs(workflow: dict) -> set:
"""Extracts input names from a workflow's on.workflow_dispatch.inputs section.
For a workflow with:
on:
workflow_dispatch:
inputs:
amdgpu_family: ...
release_type: ...
Returns: {"amdgpu_family", "release_type"}
"""
return set(_get_dispatch_inputs(workflow).keys())
def get_required_workflow_dispatch_inputs(workflow: dict) -> set:
"""Extracts required input names (no default) from workflow_dispatch.
For a workflow with:
on:
workflow_dispatch:
inputs:
amdgpu_family:
required: true
release_type:
required: true
default: dev
Returns: {"amdgpu_family"} (release_type has a default)
"""
required = set()
for name, props in _get_dispatch_inputs(workflow).items():
if isinstance(props, dict):
if props.get("required", False) and "default" not in props:
required.add(name)
return required
def get_transitive_workflow_uses(root_filenames: list[str]) -> set[str]:
"""Returns all workflow filenames transitively referenced via reusable workflow calls.
Starting from the given root workflow filenames, follows all
``uses: ./.github/workflows/<name>.yml`` references in job definitions
and returns the complete set of workflow filenames (including the roots).
"""
visited: set[str] = set()
queue = list(root_filenames)
while queue:
filename = queue.pop()
if filename in visited:
continue
visited.add(filename)
workflow_path = WORKFLOWS_DIR / filename
if not workflow_path.exists():
continue
workflow = load_workflow(workflow_path)
if not isinstance(workflow, dict):
continue
jobs = workflow.get("jobs")
if not isinstance(jobs, dict):
continue
for job_def in jobs.values():
if not isinstance(job_def, dict):
continue
uses = job_def.get("uses")
if isinstance(uses, str) and uses.startswith("./.github/workflows/"):
ref_filename = uses.removeprefix("./.github/workflows/")
queue.append(ref_filename)
return visited
def get_choice_options(workflow: dict, input_name: str) -> list | None:
"""Extracts the options list for a type: choice workflow_dispatch input.
For a workflow with:
on:
workflow_dispatch:
inputs:
amdgpu_family:
type: choice
options:
- gfx94X-dcgpu
- gfx110X-all
get_choice_options(workflow, "amdgpu_family") returns:
["gfx94X-dcgpu", "gfx110X-all"]
Returns None if the input doesn't exist or isn't type: choice.
"""
input_def = _get_dispatch_inputs(workflow).get(input_name)
if not isinstance(input_def, dict):
return None
if input_def.get("type") != "choice":
return None
options = input_def.get("options")
if not isinstance(options, list):
return None
return options