From fe84714e44c585be0c39929621bb3b5737e72857 Mon Sep 17 00:00:00 2001 From: raymondginger Date: Tue, 20 Oct 2026 19:59:19 +0800 Subject: [PATCH 1/2] test(ci): keep the docs/CI.md check table aligned with workflow jobs --- tests/test_ci_documentation.py | 71 ++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_ci_documentation.py diff --git a/tests/test_ci_documentation.py b/tests/test_ci_documentation.py new file mode 100644 index 00000000..f1280174 --- /dev/null +++ b/tests/test_ci_documentation.py @@ -0,0 +1,71 @@ +"""The docs/CI.md check table must keep pace with the workflow job inventory.""" + +from pathlib import Path + +import yaml + +# Jobs that never gate a pull request stay out of the check table. +RELEASE_ONLY_JOBS = { + "deploy-website.yml": {"build", "deploy"}, + "pypi-publish.yml": {"release-build", "pypi-publish"}, + "release-desktop.yml": {"verify-tag", "publish"}, +} + +# Every other job must be recognisable under these phrases in docs/CI.md. +# When a job is added or renamed, document it in the table and map it here, or +# list it above when pull requests genuinely do not run it. +CHECK_TABLE = { + "linting.yaml": {"lint-and-format": "formatting"}, + "python-ci.yml": { + "test": "Python 3.13", + "windows-lifecycle": "Windows lifecycle", + "package": "Python package", + }, + "security-ci.yml": { + "secret-scan": "Secret scan", + "dependency-audit": "Dependency audit", + "dependency-review": "dependency review", + }, + "web-ci.yml": {"browser": "Browser"}, + "desktop-ci.yml": { + "quality": "Desktop quality", + "bundle": "Four platform bundles", + }, +} + + +def _root(): + return Path(__file__).resolve().parents[1] + + +def _workflow_jobs(): + workflows = _root() / ".github" / "workflows" + return { + path.name: set( + (yaml.safe_load(path.read_text(encoding="utf-8")) or {}).get("jobs", {}) + ) + for path in sorted(workflows.iterdir()) + if path.suffix in {".yml", ".yaml"} + } + + +def test_every_workflow_job_is_classified(): + jobs = _workflow_jobs() + assert set(jobs) == set(CHECK_TABLE) | set(RELEASE_ONLY_JOBS), ( + "classify every workflow file in CHECK_TABLE or RELEASE_ONLY_JOBS" + ) + for filename, job_ids in jobs.items(): + documented = set(CHECK_TABLE.get(filename, ())) + allowlisted = RELEASE_ONLY_JOBS.get(filename, set()) + assert job_ids == documented | allowlisted, ( + f"{filename}: classify each job in CHECK_TABLE or RELEASE_ONLY_JOBS" + ) + + +def test_check_table_phrases_still_appear_in_the_docs(): + ci_doc = (_root() / "docs" / "CI.md").read_text(encoding="utf-8").lower() + for job_ids in CHECK_TABLE.values(): + for job_id, phrase in job_ids.items(): + assert phrase.lower() in ci_doc, ( + f"{job_id}: docs/CI.md no longer contains {phrase!r}" + ) From 5efea2653e2cbb67e7b365a1f121d9163635c245 Mon Sep 17 00:00:00 2001 From: raymondginger Date: Wed, 21 Oct 2026 22:22:41 +0800 Subject: [PATCH 2/2] test(ci): match the check table's first column and document the lint row Review fixes: parse the Check table's first column instead of searching the whole doc (intro/Rust-formatting false passes), give lint-and-format its own table row, and reword the allowlist comment to release-only so it matches dependency-audit/bundle living in CHECK_TABLE. --- docs/CI.md | 1 + tests/test_ci_documentation.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/docs/CI.md b/docs/CI.md index 1ffa3bf0..25c7b7ec 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -27,6 +27,7 @@ workflow that never started. Desktop bundle jobs retain their existing scope. | Check | What it verifies | |---|---| +| Formatting and lint (lint-and-format) | pre-commit, run on every push and PR by linting.yaml | | Python 3.13 (push) / 3.12–3.14 (PR) | The full backend suite on Ubuntu 24.04 | | Windows lifecycle | Real file locks, ACLs, recovery, background service operations, and discovery races | | Browser | Chromium interactions with the real local service and a deterministic test Agent | diff --git a/tests/test_ci_documentation.py b/tests/test_ci_documentation.py index f1280174..563e4e19 100644 --- a/tests/test_ci_documentation.py +++ b/tests/test_ci_documentation.py @@ -4,16 +4,16 @@ import yaml -# Jobs that never gate a pull request stay out of the check table. +# Release-only jobs stay out of the check table. RELEASE_ONLY_JOBS = { "deploy-website.yml": {"build", "deploy"}, "pypi-publish.yml": {"release-build", "pypi-publish"}, "release-desktop.yml": {"verify-tag", "publish"}, } -# Every other job must be recognisable under these phrases in docs/CI.md. -# When a job is added or renamed, document it in the table and map it here, or -# list it above when pull requests genuinely do not run it. +# Every other job must be recognisable in the first column of the check table +# in docs/CI.md. When a job is added or renamed, document it in the table and +# map it here, or list it above when the workflow is release-only. CHECK_TABLE = { "linting.yaml": {"lint-and-format": "formatting"}, "python-ci.yml": { @@ -49,6 +49,26 @@ def _workflow_jobs(): } +def _check_table_first_columns(): + """First column of the "Check | What it verifies" table, one entry per row.""" + ci_doc = (_root() / "docs" / "CI.md").read_text(encoding="utf-8") + first_columns = [] + in_table = False + for line in ci_doc.splitlines(): + stripped = line.strip() + if stripped.startswith("| Check |"): + in_table = True + continue + if not in_table: + continue + if not stripped.startswith("|"): + break + first_cell = stripped.lstrip("|").split("|", 1)[0].strip() + if first_cell and not set(first_cell) <= {"-"}: + first_columns.append(first_cell) + return first_columns + + def test_every_workflow_job_is_classified(): jobs = _workflow_jobs() assert set(jobs) == set(CHECK_TABLE) | set(RELEASE_ONLY_JOBS), ( @@ -63,9 +83,9 @@ def test_every_workflow_job_is_classified(): def test_check_table_phrases_still_appear_in_the_docs(): - ci_doc = (_root() / "docs" / "CI.md").read_text(encoding="utf-8").lower() + first_columns = _check_table_first_columns() for job_ids in CHECK_TABLE.values(): for job_id, phrase in job_ids.items(): - assert phrase.lower() in ci_doc, ( - f"{job_id}: docs/CI.md no longer contains {phrase!r}" + assert any(phrase.lower() in col.lower() for col in first_columns), ( + f"{job_id}: no check-table row in its first column mentions {phrase!r}" )