FreeCampus Python

Publish Documentation and Keep It Trustworthy

Connect a clean local documentation contract to CI and GitHub Pages, diagnose deployment evidence, and maintain public promises as code and routes change.
python-foundations documentation-publishing continuous-integration maintenance
Open in Colab
  • Level: Beginner
  • Estimated time: 2.5–4 hours
  • You will learn: Trace reviewed documentation through a clean CI build to one verified deployment artifact, then plan updates when public behavior or routes change.
  • Practice in: A local Git project and a learner-sized GitHub Actions workflow

Publication changes the audience and the cost of mistakes. A local page can rely on an author’s environment; a public site must be built from committed source in a clean runner, produce a named artifact, and expose the reviewed version. Even then, publication is a moment in an ongoing maintenance process—not proof that the documentation will remain current forever.

This lesson asks:

  1. Which commit, environment, commands, and output directory produced the public site?
  2. How does CI repeat the local documentation contract instead of inventing a second one?
  3. What prevents a failed build from deploying stale or unrelated files?
  4. How should a learner read setup, render, and deployment failures differently?
  5. Which documentation changes follow a public API, support, or route change?

1. Trace one reviewed commit to one artifact

A useful publication path has explicit stages:

  1. Check out the reviewed source commit.
  2. Create the documented environment.
  3. Install the project and documentation tools.
  4. Run code and documentation checks.
  5. Perform a complete Quarto render.
  6. Verify the expected _site artifact.
  7. Deploy that same artifact only when earlier stages succeeded.

The public site should be traceable to the reviewed commit and the artifact its successful gate produced.

flowchart LR
  A[Reviewed commit] --> B[Clean runner]
  B --> C[Install environment]
  C --> D[Tests and docs checks]
  D --> E[Complete render]
  E --> F[Verified _site artifact]
  F --> G[GitHub Pages]
  D -. failure .-> H[No deployment]
  E -. failure .-> H

The local contract should already work:

pytest --doctest-modules src tests -q
quarto render docs
python tests/check_rendered_docs.py

CI should repeat these responsibilities. If local instructions use Poetry while CI installs an unrelated requirements file, the two environments can drift. Share commands or keep their equivalence visible and tested.

Artifact means the generated site, not source

For the Midnight Museum configuration, Quarto writes under docs/_site. The workflow should publish that directory—not docs/, not repository root, not a stale _site created by an earlier job.

Verify it before deployment:

from pathlib import Path

site = Path("docs/_site")
expected = {
    "index.html",
    "tutorial.html",
    "how-to-rank-exhibits.html",
    "why-ranking-is-deterministic.html",
    "api.html",
}

assert site.is_dir(), "Quarto output directory is missing"
observed = {path.name for path in site.glob("*.html")}
assert expected <= observed, f"missing published pages: {sorted(expected - observed)}"

This check cannot judge the prose, but it prevents a workflow from publishing an empty or incomplete page set under a successful deployment step.

2. Read a learner-sized documentation workflow

The repository’s real workflow includes environment setup, Poetry, the complete site build, and a GitHub Pages action. A smaller project can begin here:

name: Documentation

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: docs-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Check out reviewed source
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.13"

      - name: Install project and test tools
        run: |
          python -m pip install --upgrade pip
          python -m pip install -e ".[dev]"

      - name: Test code and documentation examples
        run: pytest --doctest-modules src tests -q

      - name: Render the documentation site
        run: quarto render docs

      - name: Verify the rendered page set
        run: python tests/check_rendered_docs.py

      - name: Publish the verified site
        if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_branch: gh-pages
          publish_dir: docs/_site
          force_orphan: true

This example assumes Quarto is available on the runner and the project defines a dev extra. A complete workflow must install Quarto explicitly or use an environment/action that supplies it. Never copy setup lines without checking those assumptions.

Read each boundary:

  • Pull requests build and review but do not publish.
  • Pushes to main build and can publish after success.
  • workflow_dispatch allows a deliberate manual build; its publish condition as written excludes it.
  • A job stops after a failing step because later commands are not reached.
  • The deployment step’s if narrows publishing even when builds run elsewhere.
  • publish_dir points to the output verified immediately before it.
  • Concurrency cancels an older in-progress publication for the same ref.

Permissions deserve the smallest scope supported by the selected publishing method. This action pushes a branch, so the example grants contents: write. Other official GitHub Pages workflows use artifact upload and Pages-specific permissions instead. Follow the current documentation for the method you adopt.

Checkpoint: trace build and deploy

3. Preserve exit status through every command

A shell step can hide a failure:

quarto render docs || echo "render failed"
echo "continuing"

The echo after || succeeds, so the step may exit zero and allow deployment. Logging is not failure handling. Let the command fail, or capture status and exit nonzero after recording useful evidence.

Pipes can also conceal an earlier failure in shells that report only the final command:

quarto render docs | tee render.log

GitHub Actions’ Bash environment normally uses failure-sensitive settings, but do not rely on memory when translating the command to another runner. A simple un-piped render is clearest for the learner workflow. If a log pipeline is necessary, configure and test pipe failure behavior.

Do not combine every responsibility into one opaque line:

pytest -q && quarto render docs && python tests/check_rendered_docs.py

The chain stops correctly, but separate named workflow steps make it clearer whether code examples, rendering, or artifact inspection failed.

A stale artifact can survive an incomplete build

If a runner reuses a workspace and the new render omits api.qmd, an old api.html may remain. Clean runners reduce this risk; explicit output cleanup and a named source/output inventory make it visible locally.

A deployment job should download the artifact created by the build job for the same workflow run—not rebuild a different commit or publish a path left on a persistent machine.

4. Read the earliest useful CI evidence

Environment failure

ModuleNotFoundError: No module named 'museum_quest'

If it appears during doctest collection, inspect installation and working directory before changing a docstring result. Confirm the checked-out project was installed and the expected Python environment is active.

Missing renderer

quarto: command not found

The workflow never reached QMD content. Install Quarto through the selected setup method and pin/record an appropriate version. Do not edit navigation to repair a missing executable.

Source/configuration failure

ERROR: YAMLException: bad indentation of a mapping entry
  at docs/_quarto.yml:14:7

Now the renderer exists and has named a project file/line. Repair YAML structure, validate it locally, and run the complete gate.

Artifact failure

AssertionError: missing published pages: ['api.html']

The render command returned zero, but the named output inventory is incomplete. Inspect source inclusion, navigation/render targets, and stale-output cleanup. Do not delete api.html from the expected set unless the public map was intentionally changed and reviewed.

Deployment failure

remote: Permission to repository denied

The artifact may be correct while the publishing token or permissions are not. Inspect the deployment method’s required permission and branch settings. Do not add a personal token to committed YAML or lesson output.

Checkpoint: repair the owning layer

5. Publication does not freeze the product

Documentation freshness follows public change. Use a change-impact table:

Change Documentation to review
Private helper renamed Maintainer explanation only if it names the helper
Public parameter renamed README examples, tutorial, how-to, docstring, API reference, doctest, release guidance
Supported Python floor raised README requirements, package metadata, setup workflows, troubleshooting
Expected score policy changed Examples, tests, explanation, API contract, migration note
Public QMD route moved Navigation, internal/external links, redirects or migration plan
Deployment directory changed Quarto config, artifact check, workflow publish_dir

This repository is intentionally pre-release and does not preserve obsolete routes automatically. A published library with existing readers may require redirects, stable IDs, versioned pages, or a deprecation period. Decide from the actual compatibility requirement rather than treating either “keep everything” or “delete everything” as universal.

Separate current truth from history

The README and current API reference should describe supported behavior now. Release notes or a changelog describe what changed. Mixing years of historical instructions into the quick start makes current use harder and can lead readers to obsolete commands.

When several versions remain supported, label the documented version visibly and ensure examples are built against it. Multi-version hosting is beyond this Foundations unit, but hiding version scope is not.

Give readers a correction path

Useful feedback routes include:

  • an “Edit this page” link to the source;
  • an issue link with a documentation template;
  • a support address or forum with stated scope;
  • a named maintainer/owner for review; and
  • a page timestamp only when the project can interpret and maintain it.

A “last updated” badge generated on every build can appear fresh while the content remains stale. Prefer version/commit traceability and reviewed public claims.

6. Keep a small publication record

For a release or merge, retain:

Source commit:        4c72...
Python:               3.13
Quarto:               1.9.38
Doctest:              2 attempted, 0 failed
Pytest:               8 passed
Rendered pages:       5 expected, 5 present
Known warnings:       none
Published artifact:   docs/_site from this workflow run
Public smoke check:   index and API route loaded

The values are evidence, not decoration. Do not copy them into future records without rerunning. A smoke check of the hosted route catches hosting/path configuration that local HTML inspection cannot.

7. Compare publication methods by trust boundary

Quarto documents several ways to publish to GitHub Pages:

  • render locally and commit generated output;
  • use quarto publish from a trusted local environment; or
  • render and deploy through CI.

This course prefers CI for the connected project because it ties clean setup, checks, rendering, and deployment to a reviewed commit. That preference is not a claim that one hosting method fits every organization. The important questions remain: who can publish, which source was built, which tools/versions ran, what artifact passed, and how a failure prevents replacement of the public site.

Checkpoint: maintain public trust

8. Lab: repair a workflow that publishes the wrong evidence

Audit this defective design:

steps:
  - uses: actions/checkout@v4
  - name: Render
    run: quarto render docs || echo "ignored"
  - name: Publish
    uses: peaceiris/actions-gh-pages@v4
    with:
      github_token: plain-text-token
      publish_dir: docs

Problems include:

  • no Python/Quarto environment setup;
  • no package installation, pytest, doctest, links, or page inventory;
  • a swallowed render failure;
  • a committed secret placeholder that invites unsafe replacement;
  • source docs/ published instead of generated docs/_site;
  • no branch/event condition preventing pull-request publication;
  • no connection between a verified artifact and deployment.

Write a corrected learner-sized workflow using the staged design from section 2. Then simulate its local contract:

pytest --doctest-modules src tests -q
quarto render docs
python tests/check_rendered_docs.py

Create one failure at a time:

  1. remove the package installation and observe the import boundary;
  2. misspell sidebar.contents and observe the render boundary;
  3. remove api.qmd from the source set and observe the artifact boundary;
  4. set publish_dir: docs and explain why a successful deployment would still publish the wrong thing.

Record each as environment, check, render, artifact, or deployment evidence.

Hint: make publication the final consumer

Draw arrows from checkout through setup, tests, render, and artifact inspection. The publish step should have only one incoming artifact path and no way to run after an earlier failure.

Reveal the critical corrected boundaries
- name: Test code and documentation examples
  run: pytest --doctest-modules src tests -q

- name: Render documentation
  run: quarto render docs

- name: Verify rendered pages
  run: python tests/check_rendered_docs.py

- name: Publish verified site
  if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
  uses: peaceiris/actions-gh-pages@v4
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: docs/_site

This fragment still depends on earlier checkout and environment setup. Keep the complete workflow readable from top to bottom rather than publishing only the final fragment.

Key points

  • Trace the public site to one reviewed commit, clean environment, complete gate, and named generated artifact.
  • Reuse the local documentation contract in CI and keep unavoidable environment differences explicit.
  • Never swallow the exit status of tests or rendering before deployment.
  • Read setup, source, render, artifact, and permission failures at their owning boundaries.
  • Publish the verified _site directory, not source, cache, or an artifact from another run.
  • Review documentation according to public change and make route/version compatibility decisions explicitly.
  • Publication creates a feedback and maintenance responsibility; it does not make content permanently current.

References

Next: Publish the Puzzle Garden Field Guide

Back to top