Files
kandev/.github/scripts/pr-docs-workflow-contract_test.py
2026-09-16 09:42:22 +01:00

120 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Contract tests for the pull request documentation coverage workflow."""
from __future__ import annotations
from pathlib import Path
import re
import unittest
REPO_ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "pr-docs.yml"
LINT_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "lint-action-pinning.yml"
class PullRequestDocumentationWorkflowContractTest(unittest.TestCase):
def setUp(self) -> None:
self.assertTrue(WORKFLOW.is_file(), "Documentation coverage workflow is missing")
self.workflow = WORKFLOW.read_text(encoding="utf-8")
# @covers AC-CI-PR-DOCS-001.1, AC-CI-PR-DOCS-002.2, AC-CI-PR-DOCS-004.4
def test_runs_for_relevant_pr_and_manual_retry_events(self) -> None:
trigger = self.workflow.partition("on:\n")[2].partition("\nconcurrency:")[0]
self.assertIn("pull_request_target:", trigger)
for event in (
"opened",
"reopened",
"synchronize",
"edited",
"labeled",
"unlabeled",
"ready_for_review",
):
if event == "ready_for_review":
self.assertNotIn(event, trigger)
else:
self.assertIn(event, trigger)
self.assertIn("merge_group:", trigger)
self.assertIn("checks_requested", trigger)
self.assertIn("workflow_dispatch:", trigger)
self.assertIn("pr_number:", trigger)
self.assertNotIn("paths:", trigger)
# @covers AC-CI-PR-DOCS-002.2, AC-CI-PR-DOCS-004.4
def test_unrelated_label_events_are_rejected_at_the_job_boundary(self) -> None:
coverage_job = self.workflow.partition("jobs:\n")[2]
self.assertIn("github.event_name != 'pull_request_target'", coverage_job)
self.assertIn("github.event.action != 'labeled'", coverage_job)
self.assertIn("github.event.action != 'unlabeled'", coverage_job)
self.assertIn("github.event.label.name == 'no-docs-allow'", coverage_job)
self.assertLess(coverage_job.index("github.event.label.name"), coverage_job.index("runs-on:"))
# @covers AC-CI-PR-DOCS-004.4
def test_description_edits_are_rejected_but_base_retargets_run(self) -> None:
coverage_job = self.workflow.partition("jobs:\n")[2]
self.assertIn("github.event.action != 'edited'", coverage_job)
self.assertIn("github.event.changes.base != null", coverage_job)
self.assertLess(coverage_job.index("github.event.changes.base"), coverage_job.index("runs-on:"))
# @covers AC-CI-PR-DOCS-003.1, AC-CI-PR-DOCS-003.4
def test_serializes_prs_without_cancelling_merge_group_runs(self) -> None:
self.assertIn("group: >-", self.workflow)
self.assertIn("github.event.merge_group.base_ref", self.workflow)
self.assertIn("github.event.pull_request.base.ref", self.workflow)
self.assertIn("queue: max", self.workflow)
self.assertIn("cancel-in-progress: false", self.workflow)
# @covers AC-CI-PR-DOCS-003.3
def test_uses_trusted_least_privilege_checkout(self) -> None:
permissions = self.workflow.partition("permissions:\n")[2].partition("\njobs:")[0]
self.assertEqual(
permissions.strip(),
"contents: read\n pull-requests: read\n statuses: write",
)
self.assertRegex(
self.workflow,
r"uses: actions/checkout@[0-9a-f]{40} # v[0-9]+",
)
self.assertIn("persist-credentials: false", self.workflow)
self.assertIn("github.event.merge_group.base_sha", self.workflow)
self.assertNotIn("github.event.pull_request.head.sha", self.workflow)
self.assertIn("ref: ${{ env.CHECKOUT_REF }}", self.workflow)
self.assertNotIn("pull_request:\n", self.workflow)
self.assertNotIn("pnpm install", self.workflow)
self.assertNotIn("npm install", self.workflow)
self.assertIn("node .github/scripts/pr-docs.cjs", self.workflow)
self.assertIn("name: Publish PR documentation coverage status", self.workflow)
self.assertNotIn("name: PR documentation coverage\n", self.workflow)
# @covers AC-CI-PR-DOCS-001.1, AC-CI-PR-DOCS-002.1, AC-CI-PR-DOCS-003.2
def test_dispatch_is_restricted_to_the_default_branch_and_script_owns_statuses(self) -> None:
self.assertRegex(
self.workflow,
re.compile(
r"github\.event_name != 'workflow_dispatch'.*github\.ref.*default_branch",
re.DOTALL,
),
)
self.assertIn("PR documentation coverage", (REPO_ROOT / ".github" / "scripts" / "pr-docs.cjs").read_text(encoding="utf-8"))
self.assertIn("statuses: write", self.workflow)
def test_contract_is_registered_in_the_required_lint_workflow(self) -> None:
lint_workflow = LINT_WORKFLOW.read_text(encoding="utf-8")
self.assertRegex(
lint_workflow,
r"(?m)^ - name: Test pull request documentation workflow contract$\n"
r"^ run: python3 .github/scripts/pr-docs-workflow-contract_test.py$",
)
self.assertRegex(
lint_workflow,
r"(?m)^ - name: Test pull request documentation validator$\n"
r"^ run: node --test .github/scripts/pr-docs.test.cjs$",
)
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
self.assertIn("@node --test .github/scripts/pr-docs.test.cjs", makefile)
if __name__ == "__main__":
unittest.main()