Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/audits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
- name: Install faircode + pytest
run: |
python -m pip install --upgrade pip
pip install -e ".[excel,parquet,proxy,mcp]" pytest pytest-cov
pip install -e ".[test,excel,parquet,proxy,mcp]"

# One run: the whole suite with line coverage printed at the end. A test failure fails the
# job; the coverage figure itself is informational, not a gate (#932).
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ parquet = ["pyarrow>=10.0"]
proxy = ["scipy>=1.10"]
benchmark = ["scikit-learn>=1.8.0", "pyyaml>=6.0", "fairlearn>=0.14.0", "matplotlib>=3.10"]
mcp = ["mcp>=2.0"]
test = ["pytest>=8.0", "pytest-cov>=5.0", "certifi>=2023.1.1"] # certifi: scripts/check_external_links.py trust store fallback
test = ["pytest>=8.0", "pytest-cov>=5.0", "certifi>=2023.1.1", "pyyaml>=6.0"] # certifi: scripts/check_external_links.py trust store fallback; pyyaml: workflow safeguard tests
browser = ["playwright>=1.40"] # tests/browser; also run `python -m playwright install chromium`

[project.urls]
Expand Down
66 changes: 66 additions & 0 deletions tests/test_scheduled_workflow_safeguards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from pathlib import Path

import pytest
import yaml


WORKFLOWS_DIR = Path(__file__).parents[1] / ".github/workflows"
EXPECTED_SCHEDULED_PERMISSIONS = {
"codeowners-access.yml": {"contents": "read"},
"codeql.yml": {
"actions": "read",
"contents": "read",
"security-events": "write",
},
"external-links.yml": {"contents": "read", "issues": "write"},
"traction-live.yml": {"contents": "read", "issues": "write"},
}


def _scheduled_workflows():
workflows = []
for path in sorted(WORKFLOWS_DIR.glob("*.yml")):
workflow = yaml.load(path.read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
if "schedule" in workflow.get("on", {}):
workflows.append((path, workflow))
return workflows


SCHEDULED_WORKFLOWS = _scheduled_workflows()


def test_scheduled_workflows_have_explicit_minimal_permissions():
actual = {path.name for path, _ in SCHEDULED_WORKFLOWS}
assert actual == set(EXPECTED_SCHEDULED_PERMISSIONS)

for path, workflow in SCHEDULED_WORKFLOWS:
permissions = workflow.get("permissions")
assert permissions == EXPECTED_SCHEDULED_PERMISSIONS[path.name]
assert permissions["contents"] == "read"


@pytest.mark.parametrize(
"path,workflow",
SCHEDULED_WORKFLOWS,
ids=[path.name for path, _ in SCHEDULED_WORKFLOWS],
)
def test_scheduled_workflows_have_concurrency_controls(path, workflow):
concurrency = workflow.get("concurrency")
assert isinstance(concurrency, dict), path.name
assert concurrency.get("group", "").strip(), path.name
assert "cancel-in-progress" in concurrency, path.name


@pytest.mark.parametrize(
"path,workflow",
SCHEDULED_WORKFLOWS,
ids=[path.name for path, _ in SCHEDULED_WORKFLOWS],
)
def test_scheduled_workflow_jobs_have_timeouts(path, workflow):
jobs = workflow.get("jobs")
assert isinstance(jobs, dict) and jobs, path.name

for job_name, job in jobs.items():
timeout = job.get("timeout-minutes")
assert timeout is not None, f"{path.name}:{job_name} has no timeout"
assert 1 <= int(timeout) <= 360, f"{path.name}:{job_name} has invalid timeout"
Loading