From 1b507798d5eb09b3bc6e91fff0815d417088bfef Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Mon, 14 Sep 2026 00:00:39 +0200 Subject: [PATCH] build: build the documentation in CI Nothing built these docs, so a change to the CLI parser or to a cross-reference could break them and no check would notice. #40 recorded the reason: reference.rst is generated by sphinxcontrib-autoprogram from the parser in cli/sphinx.py, and render() builds that parser by issuing an HTTP OPTIONS request per resource against a running Ascender. There is no static command table to fall back on, since the CLI discovers its own commands the same way. That is one page out of seven. The other six are plain reStructuredText and need nothing, so conf.py now drops reference.rst when no credentials are present and the rest build offline. A build with CONTROLLER_HOST set still produces the complete documentation, unchanged. The job runs with -W, which is what earns its keep: a dead cross-reference or a malformed directive fails rather than rendering wrong. Verified by pointing a :ref: at a label that does not exist, which fails the build with `undefined label ... [ref.ref]` and exit 1. Moving the toctree entry behind an `only` directive was the tidier option and does not work: toctree entries resolve while the source is read and `only` is evaluated later, so the warning fires anyway. It is suppressed by name instead, with the reasoning in conf.py next to it. Gating rather than advisory, unlike the type checker in #42. That one started with 136 diagnostics to work through, so blocking on it would have blocked everything; this build is clean today, and an advisory job that always passes protects nothing. --- .github/workflows/ci.yml | 21 ++++++++++++++++++ ascenderkit/cli/docs/README.md | 33 +++++++++++++++++++++++------ ascenderkit/cli/docs/source/conf.py | 23 ++++++++++++++++++++ tox.ini | 18 ++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d458092..5306a34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,27 @@ jobs: - name: Run the type checker run: ty check ascenderkit + docs: + name: Docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.x" + + - name: Install the package and the docs extra + run: pip install -e ".[docs]" + + - name: Build the documentation + # No credentials here, so reference.rst is left out and the six + # hand-written pages are built. -W turns a dead cross-reference or a + # malformed directive into a failure. See docs/source/conf.py. + run: >- + sphinx-build -b html -W --keep-going + ascenderkit/cli/docs/source "${RUNNER_TEMP}/html" + build: name: Build distribution runs-on: ubuntu-latest diff --git a/ascenderkit/cli/docs/README.md b/ascenderkit/cli/docs/README.md index 4c9601f..a397abd 100644 --- a/ascenderkit/cli/docs/README.md +++ b/ascenderkit/cli/docs/README.md @@ -1,13 +1,32 @@ Building the Documentation -------------------------- -To build the docs, spin up a real Ascender server, install the `docs` extra with `pip install -e ".[docs]"`, and run: +Install the `docs` extra with `pip install -e ".[docs]"`, then run `tox -e docs`, +or invoke Sphinx directly: - ~ CONTROLLER_HOST=https://ascender.example.org CONTROLLER_USERNAME=example CONTROLLER_PASSWORD=secret make clean html + ~ sphinx-build -b html -W --keep-going source build/html ~ cd build/html/ && python -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) .. -What still needs a server -------------------------- -The extension itself imports without a server. `render()` runs when the -`autoprogram` directive in `reference.rst` asks for the parser, so that one page -is what needs `CONTROLLER_HOST`, not the Sphinx run. +That builds the six hand-written pages and leaves out `reference.rst`, which is +what CI does on every pull request. `-W` is the point of running it: a dead +cross-reference or a malformed directive fails the build rather than rendering +wrong. + +To build the reference guide as well, point the same command at a real Ascender: + + ~ CONTROLLER_HOST=https://ascender.example.org CONTROLLER_USERNAME=example \ + CONTROLLER_PASSWORD=secret sphinx-build -b html -W source build/html + +What needs a server, and why +---------------------------- +`reference.rst` is generated by `sphinxcontrib-autoprogram` from the parser in +`ascenderkit/cli/sphinx.py`, and `render()` builds that parser by issuing an HTTP +OPTIONS request per resource against a running Ascender. There is no static +command table to fall back on: the CLI discovers its own commands the same way, +which is why the reference guide carries a disclaimer about varying with API +version, settings and user access level. + +The extension itself imports without a server. `render()` runs only when the +`autoprogram` directive asks for the parser, so that one page is what needs +`CONTROLLER_HOST`, not the Sphinx run. `conf.py` drops the page when no +credentials are present, which is what makes the offline build work. diff --git a/ascenderkit/cli/docs/source/conf.py b/ascenderkit/cli/docs/source/conf.py index e03f856..515a97c 100644 --- a/ascenderkit/cli/docs/source/conf.py +++ b/ascenderkit/cli/docs/source/conf.py @@ -4,6 +4,8 @@ # list see the documentation: # http://www.sphinx-doc.org/en/master/config +import os + # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, @@ -53,3 +55,24 @@ rst_epilog = ''' .. |prog| replace:: ascender ''' + +# -- Building without a server ----------------------------------------------- + +# reference.rst is generated by sphinxcontrib-autoprogram from the parser in +# ascenderkit.cli.sphinx, which is built by issuing an HTTP OPTIONS request per +# resource against a running Ascender. There is no static command table to fall +# back on: the CLI discovers its own commands the same way. So that one page is +# the only part of these docs that needs a server, and asking for it without one +# exits with a message naming CONTROLLER_HOST. +# +# Rather than fail the whole build, drop the page when there are no credentials. +# The six hand-written pages then build offline, which is what CI does, and a +# build with CONTROLLER_HOST set still produces the complete documentation. +# +# The toctree in index.rst names reference unconditionally. Moving that entry +# behind an `only` directive does not help, because toctree entries resolve +# while the source is read and `only` is evaluated later, so the warning is +# suppressed by name instead. +if not (os.environ.get('CONTROLLER_HOST') or os.environ.get('TOWER_HOST')): + exclude_patterns = ['reference.rst'] + suppress_warnings = ['toc.excluded'] diff --git a/tox.ini b/tox.ini index 0349d22..fb63648 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,24 @@ commands = ruff check ascenderkit - coverage erase +[testenv:docs] +# Builds the documentation with warnings as errors, which is what makes this +# worth running: a dead cross-reference or malformed directive fails here rather +# than rendering wrong. reference.rst is dropped unless CONTROLLER_HOST is set, +# because that page is generated from a live server; see the comment in +# docs/source/conf.py. Pass the credentials through to build it too. +deps = + .[docs] +passenv = + CONTROLLER_HOST + CONTROLLER_USERNAME + CONTROLLER_PASSWORD + TOWER_HOST + TOWER_USERNAME + TOWER_PASSWORD +commands = + sphinx-build -b html -W --keep-going ascenderkit/cli/docs/source {envtmpdir}/html + [testenv:typecheck] # Advisory: the type checker runs over ascenderkit and reports what it finds # without failing the merge, so the diagnostics already in the tree can be