From 8fc0d938f0dd40aefd20c7d9fddab79820a777eb Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Fri, 13 Nov 2026 09:27:07 -0800 Subject: [PATCH] ci: require successful documentation validation and builds Validate candidate version configuration independently of the contributor trust gate, reuse the base branch validator, and fail the final docs check on skipped builds. Upload generated HTML and add configuration regressions without modifying versions.json. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 63 ++++++++++++++++++++++++++++++++++++ README.md | 13 +++++++- tests/test_versioned_docs.py | 13 ++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68d610f..0c0dcfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,51 @@ on: - reopened - synchronize - labeled + - unlabeled push: branches: - main jobs: + docs-config: + name: Documentation version configuration + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Checkout trusted validator + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha || github.sha }} + path: trusted + persist-credentials: false + sparse-checkout: scripts/generate_versioned_api_docs.py + sparse-checkout-cone-mode: false + - name: Checkout candidate configuration + uses: actions/checkout@v4 + with: + path: candidate + persist-credentials: false + sparse-checkout: api-docs/versions.json + sparse-checkout-cone-mode: false + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Validate candidate configuration with trusted code + working-directory: trusted + run: | + python3 -I - <<'PY' + import importlib.util + from pathlib import Path + + spec = importlib.util.spec_from_file_location( + "docs", "scripts/generate_versioned_api_docs.py" + ) + docs = importlib.util.module_from_spec(spec) + spec.loader.exec_module(docs) + docs.load_versions(Path("../candidate/api-docs/versions.json")) + PY + docs: name: Versioned API documentation permissions: @@ -43,6 +83,29 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: python3 scripts/generate_versioned_api_docs.py + - name: Upload documentation preview + uses: actions/upload-artifact@v4 + with: + name: versioned-api-documentation + path: api-docs/_build/versioned/ + if-no-files-found: error + + docs-required: + name: Documentation checks + needs: [docs-config, docs] + if: always() + permissions: {} + runs-on: ubuntu-latest + steps: + - name: Require successful validation and build + env: + CONFIG_RESULT: ${{ needs.docs-config.result }} + BUILD_RESULT: ${{ needs.docs.result }} + run: | + if [ "$CONFIG_RESULT" != success ] || [ "$BUILD_RESULT" != success ]; then + echo "::error::Documentation checks did not succeed (configuration: $CONFIG_RESULT, build: $BUILD_RESULT). A maintainer must authorize gated builds with the 'safe to test' label." + exit 1 + fi tests: name: Unit tests diff --git a/README.md b/README.md index 4fcd14d..e460362 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,18 @@ node --test tests/test_version_selector.js ``` The selector tests require Node.js 18 or newer. CI runs these tests and builds -the versioned documentation on pull requests. +the versioned documentation on authorized pull requests, uploading the generated +site as the `versioned-api-documentation` artifact. For contributors outside the +existing trust gate, a maintainer must add the `safe to test` label to authorize +the full build. + +The **Documentation version configuration** check runs independently of that +gate. It validates the candidate `api-docs/versions.json` with the base branch's +validator, without importing candidate Python code. The **Documentation checks** +job fails unless both configuration validation and the full docs build succeed, +including when the build is skipped. Configure branch protection or repository +rules to require **Documentation checks** so skipped builds cannot satisfy the +merge requirement. ## License diff --git a/tests/test_versioned_docs.py b/tests/test_versioned_docs.py index 00564b6..d5b8e64 100644 --- a/tests/test_versioned_docs.py +++ b/tests/test_versioned_docs.py @@ -12,6 +12,9 @@ class VersionedDocsTests(unittest.TestCase): + def test_checked_in_version_list_is_valid(self): + docs.load_versions(docs.ROOT / "api-docs/versions.json") + def test_version_list_preserves_default_and_order(self): with tempfile.TemporaryDirectory() as directory: config = Path(directory) / "versions.json" @@ -34,6 +37,16 @@ def test_invalid_version_lists_are_rejected(self): with self.assertRaises(ValueError): docs.load_versions(config) + def test_malformed_version_json_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + config = Path(directory) / "versions.json" + config.write_text( + '{"versions": ["v0.26.17","versions": ["v0.26.16"]}', + encoding="utf-8", + ) + with self.assertRaises(json.JSONDecodeError): + docs.load_versions(config) + @patch.object(docs.subprocess, "run") def test_published_release_is_verified(self, run): run.return_value = Mock(