Versioned documentation¶
Docs publish only the current version and carry older built versions forward from gh-pages¶
Id: 858025c1-19a6-46f6-a237-01ba6477262a
Type: decision
Status: active
Evidence: confirmed
Source: ADR-027 §1–2 (docs/adr/027-versioned-documentation-and-last-updated-stamps.md); issue #165; the Litestar docs build is the model
Revisit when: a Sphinx multiversion tool stops rebuilding old tags, or conf.py stops running build-time generators against the live app
See: docs-stack.md#sphinx-with-the-bare-shibuya-theme-replaces-mkdocs — 5133caa8-3231-4d4d-877b-041b315cb87a — as of 2026-10-11
See: docs-stack.md#pr-docs-previews-deploy-to-a-subfolder-of-the-projects-own-gh-pages-same-repo-only — 55a74975-a6e0-4f18-8254-fa27f047e829 — as of 2026-10-11
A generated tools/build_docs.py computes the version slug, enumerates the
version directories already on gh-pages, writes a generated (gitignored)
versions.json for Shibuya's native switcher, builds only the current ref,
copies every other version directory across from gh-pages, writes a root
redirect to latest (the highest non-prerelease by PEP 440 ordering), and
deploys with previews excluded from cleaning. Versioned publishing is how a
docs-enabled project always publishes; it is not a toggle. The version list is
stateless: gh-pages is the source of truth, and local builds hide the
switcher because no versions.json exists.
Reason: sphinx-multiversion (effectively unmaintained) and
sphinx-polyversion both rebuild every selected tag on every deploy. This
template's conf.py runs subprocess generators against the live app, so
rebuilding a historical tag re-runs that tag's generators against whatever the
build resolves — fragile, slow, and it forces every past conf.py to keep
building forever. Litestar's build-current-preserve-rest model avoids that.
Litestar commits and hand-edits its version list, which is untenable with
per-minor granularity and automated releases, and having CI commit back to a
protected main adds real machinery.
Rejected alternative: sphinx-multiversion or sphinx-polyversion — the
rebuild model above.
Rejected alternative: a committed versions.json — the hand-edit or
CI-commit cost above.
Consequence: gh-pages becomes load-bearing state; both deploy paths must
preserve the numeric version-slug directories. An older version's switcher is
frozen at its build time, so the switcher appears only from the first versioned
release onward. Old versions are never rebuilt, so a past conf.py need not
keep building under future tooling. The build script lives outside src/, so
the 99% coverage gate does not cover it; it stays ruff and mypy clean.
Version granularity is a scaffold-time question¶
Id: 5d18c9af-73e9-476b-9ba9-544c776812e6
Type: decision
Status: active
Evidence: confirmed
Source: ADR-027 §3 (docs/adr/027-versioned-documentation-and-last-updated-stamps.md)
Revisit when: the pre-1.0 minor-is-breaking bump policy changes
See: docs-versioning.md#docs-publish-only-the-current-version-and-carry-older-built-versions-forward-from-gh-pages — 858025c1-19a6-46f6-a237-01ba6477262a — as of 2026-10-11
See: release-automation.md#release-please-is-the-sole-release-orchestrator — 7ec8a937-fd4d-41f2-96b7-824fe3be913b — as of 2026-10-11
docs_version_granularity (asked with include_docs) offers minor (X.Y,
default), major (X) and full (X.Y.Z). The choice bakes a slicing
constant into the generated build script, overridable at build time only by the
manual redeploy.
Reason: granularity is the one knob adopters reasonably differ on. minor
matches the pre-1.0 posture where minors are the breaking bump; a large project
picks major, an archival one full. Prerelease slugs survive only under
full and are excluded from latest.
The manual redeploy rebuilds the latest published release, never HEAD¶
Id: f0ad70ba-c790-42d2-94f0-152e9527be4f
Type: decision
Status: active
Evidence: confirmed
Source: ADR-027 §4 (docs/adr/027-versioned-documentation-and-last-updated-stamps.md)
Revisit when: release-please stops creating the tag before the release is published, or the build script's granularity stops being baked in
See: docs-versioning.md#docs-publish-only-the-current-version-and-carry-older-built-versions-forward-from-gh-pages — 858025c1-19a6-46f6-a237-01ba6477262a — as of 2026-10-11
See: release-automation.md#the-release-is-created-as-a-draft-finalized-after-artifacts-attach — 398b3cd9-e57b-4856-b2c7-76752c0ec37f — as of 2026-10-11
gh-pages.yml (manual dispatch only) routes through the build script, checks
out the tag of the latest published release via the releases API, sets up the
interpreter after that checkout, restores the build script itself from the
dispatch ref, and passes the tag's baked granularity back through the
environment.
Reason: a naive root publish would wipe every version directory. The tag
comes from the API rather than git describe because release-please creates
the tag the moment the release PR merges and leaves the release a draft until
publishing succeeds — so git describe would pick an unreleased version in
exactly the window a redeploy is most likely (right after a failed release).
The interpreter setup follows the checkout so the tagged docs build on the
Python the release shipped on. The script is orchestration, same layer as the
workflow, and an older tagged copy could carry the version-dropping bug the
loud-fetch rule prevents; its granularity is not orchestration — which slug a
release occupies is fixed at publish time — so an adopter who changed the
answer later must not republish a tag under a second slug and repoint latest.
Rejected alternative: git describe --tags — selects draft tags.
Rejected alternative: use the dispatch revision's granularity — duplicates a released version under a new slug.
A failed gh-pages fetch must abort, never fall through as "branch absent"¶
Id: 135cfbb5-6c5c-45d8-9239-cf225a819818
Type: decision
Type: incident
Status: active
Evidence: confirmed
Source: ADR-027 §4a (docs/adr/027-versioned-documentation-and-last-updated-stamps.md)
Revisit when: the deploy stops cleaning the target, or git fetch learns to distinguish a missing ref from a transport error
See: docs-versioning.md#docs-publish-only-the-current-version-and-carry-older-built-versions-forward-from-gh-pages — 858025c1-19a6-46f6-a237-01ba6477262a — as of 2026-10-11
Both deploy paths probe the branch with git ls-remote --exit-code and branch
on its exit code: 2 means no such ref, anything else aborts. One layer up, the
build script's preserve step raises when a version subtree it was handed cannot
be read; only the latest alias is optional.
Reason: the previous fetch || echo swallowed every failure, not only a
missing ref. The build script then assembled a site with no prior versions and
the clean deploy deleted every published version directory — one transient
transport error destroys the documentation archive for every prior release.
Every slug the preserve step receives came from a listing of that very ref, so
it provably exists and silently omitting it is the same deletion by another
route.
Per-page "last updated" comes from git history¶
Id: 7e34868f-a26a-4c89-8189-f5778ad9a04b
Type: decision
Status: active
Evidence: confirmed
Source: ADR-027 §5 (docs/adr/027-versioned-documentation-and-last-updated-stamps.md)
Revisit when: a docs job checks out with shallow history
See: docs-stack.md#sphinx-warnings-use-an-exact-reviewable-allowlist — 2a555902-38e7-47d4-a231-445a8e6caa0f — as of 2026-10-11
sphinx-last-updated-by-git is in the docs group; every page footer shows the
commit date of its source. Any untracked-source warning from the generated
fragments goes through the warning allowlist, not blanket suppression.
Reason: readers could not tell how fresh a page was; git history is a cheap offline freshness signal.
Consequence: every docs build needs full history (fetch-depth: 0),
including the CI job that runs the warning gate.
The Sphinx warning gate runs in a job the required check context aggregates¶
Id: bc382803-5aa9-4582-9bad-1c9efef8fc49
Type: decision
Type: incident
Status: active
Evidence: confirmed
Source: ADR-027, Consequences (docs/adr/027-versioned-documentation-and-last-updated-stamps.md)
Revisit when: docs-doctest is renamed or leaves the check aggregation
See: docs-stack.md#sphinx-warnings-use-an-exact-reviewable-allowlist — 2a555902-38e7-47d4-a231-445a8e6caa0f — as of 2026-10-11
See: ci-topology.md#per-component-markers-path-filtered-jobs-and-one-coverage-gate-per-component — 467b6cc3-775b-4e4c-9a8e-b0d1c3d733ac — as of 2026-10-11
tox -e docs-build (Sphinx with warnings captured plus the allowlist checker)
runs in ci.yml's docs-doctest job, which check aggregates.
Reason: it previously ran only in the docs-preview workflow, which forks skip and no protected branch requires, so a PR introducing broken references kept every required context green. The doctest job on its own runs the doctest builder with no warnings-as-errors and no allowlist, so it never covered this. A gate must be checked by which context a protected branch sees, not by whether the job runs.