"""AC-4: GitHub Actions workflows under `.github/workflows/` are valid. YAML syntactic validity + ADR-002 dual-binary build matrix check always run. `actionlint` semantic validation runs only when the binary is on PATH; CI installs it as a job step. """ from __future__ import annotations import shutil import subprocess from pathlib import Path import pytest import yaml REPO_ROOT = Path(__file__).resolve().parents[2] WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows" WORKFLOWS = sorted(WORKFLOWS_DIR.glob("*.yml")) def test_workflows_dir_populated() -> None: # Assert names = {p.name for p in WORKFLOWS} assert {"ci.yml", "ci-tier2.yml", "release.yml", "cve-rescan.yml"} <= names @pytest.mark.parametrize("workflow", WORKFLOWS, ids=[p.name for p in WORKFLOWS]) def test_workflow_yaml_parses(workflow: Path) -> None: # Act data = yaml.safe_load(workflow.read_text()) # Assert assert isinstance(data, dict), f"{workflow.name} must parse to a mapping" # GitHub Actions reserves `on` as a top-level key; PyYAML preserves it as a # bool-style key, so also accept the bool key `True` produced by safe_load. assert "on" in data or True in data, f"{workflow.name} missing trigger block" assert data.get("jobs"), f"{workflow.name} must declare jobs" def test_ci_yml_has_dual_binary_matrix() -> None: """ADR-002: deployment + research must both build in ci.yml.""" # Arrange raw = (WORKFLOWS_DIR / "ci.yml").read_text() # Assert # Match the matrix dimension we care about without depending on YAML key order. assert "deployment" in raw, "ci.yml matrix must include `deployment` kind" assert "research" in raw, "ci.yml matrix must include `research` kind" assert "matrix:" in raw, "ci.yml build job must use a strategy matrix" def test_actionlint_passes() -> None: # Arrange actionlint = shutil.which("actionlint") if actionlint is None: pytest.skip("actionlint not on PATH; CI installs it before the lint job") # Act result = subprocess.run( [actionlint, *(str(p) for p in WORKFLOWS)], capture_output=True, text=True, check=False, ) # Assert assert result.returncode == 0, ( f"actionlint reported errors:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" )