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