Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ jobs:
run: pip install -r requirements-docs.txt
- name: Build (strict)
run: mkdocs build --strict --site-dir site
- name: Docs site actually contains its documents
# `mkdocs build --strict` succeeded for months while publishing empty
# pages: all nine snippet includes used a `../` path that pymdownx
# refuses, and check_paths:false made the refusal silent.
run: python scripts/check_docs_site.py --site site
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../ARCHITECTURE.md"
--8<-- "ARCHITECTURE.md"
2 changes: 1 addition & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../CHANGELOG.md"
--8<-- "CHANGELOG.md"
2 changes: 1 addition & 1 deletion docs/code-of-conduct.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../CODE_OF_CONDUCT.md"
--8<-- "CODE_OF_CONDUCT.md"
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../CONTRIBUTING.md"
--8<-- "CONTRIBUTING.md"
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ devrepro doctor # full read-only diagnostic scan
- [Interoperability](interop.md) — how we relate to Nix, mise, Devbox, devenv…
- [Roadmap](roadmap.md) — where we're going

--8<-- "../README.md"
--8<-- "README.md"
2 changes: 1 addition & 1 deletion docs/interop.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../INTEROP.md"
--8<-- "INTEROP.md"
2 changes: 1 addition & 1 deletion docs/product-gaps.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../PRODUCT_GAPS.md"
--8<-- "PRODUCT_GAPS.md"
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../ROADMAP.md"
--8<-- "ROADMAP.md"
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
--8<-- "../SECURITY.md"
--8<-- "SECURITY.md"
19 changes: 17 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,27 @@ markdown_extensions:
- admonition
- pymdownx.superfences
- pymdownx.snippets:
base_path: docs
check_paths: false
base_path: .
check_paths: true
- tables
- toc:
permalink: true

validation:
omitted_files: warn
absolute_links: warn
links:
# Root documents (README, ARCHITECTURE, CONTRIBUTING...) are included into
# this site by snippet and carry links written relative to the repository
# root, where they are correct and where most people read them. Inside the
# site those targets sit outside the docs tree, so mkdocs reports them as
# not-found and --strict aborts.
#
# Downgraded to info deliberately, and paired with `check_paths: true`
# above, which is the check that actually matters: a snippet that cannot be
# found now fails the build. It previously did not. With check_paths off,
# all nine includes in this site silently resolved to nothing, and the
# published pages for Architecture, Roadmap, Contributing, Security,
# Interop, Product gaps, Changelog and Code of conduct were empty while the
# build stayed green.
not_found: info
94 changes: 94 additions & 0 deletions scripts/check_docs_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Assert the built docs site actually contains its documents.

`mkdocs build --strict` passing does not mean the site has content in it. This
project's site was green while publishing empty pages: every one of its nine
`--8<--` snippet includes used a `../` path, which pymdownx refuses because it
escapes `base_path`, and `check_paths: false` made that refusal silent. The
Architecture page shipped with 68 words of navigation chrome and none of
ARCHITECTURE.md.

A build that succeeds while producing nothing is the same defect this
repository has fixed twice elsewhere -- a CI step named for a check it never
performed. So the site is checked for content, not just for exit status.

python scripts/check_docs_site.py # build, then verify
python scripts/check_docs_site.py --site DIR # verify an existing build
"""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent

#: A page carrying only nav chrome lands around 60-70 words. Real documents
#: here run to hundreds. 120 sits clear of the chrome and below the shortest
#: genuine page, so it catches an empty include without tuning per page.
MIN_WORDS = 120

TAG = re.compile(r"<[^>]+>")


def words_in(html: str) -> int:
body = html.split("<article", 1)[-1].split("</article>", 1)[0]
return len(TAG.sub(" ", body).split())


def build(into: Path) -> None:
result = subprocess.run( # noqa: S603 - fixed argv, no shell, no user input
[sys.executable, "-m", "mkdocs", "build", "--strict", "--site-dir", str(into)],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
print(result.stderr or result.stdout, file=sys.stderr)
raise SystemExit("mkdocs build failed")


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--site", type=Path, default=None)
args = parser.parse_args()

with tempfile.TemporaryDirectory() as tmp:
site = args.site or Path(tmp) / "site"
if args.site is None:
build(site)

pages = sorted(site.rglob("index.html"))
if not pages:
print("no pages were built", file=sys.stderr)
return 1

thin: list[tuple[str, int]] = []
for page in pages:
name = page.parent.relative_to(site).as_posix() or "(home)"
if name.startswith(("assets", "search")):
continue
count = words_in(page.read_text(encoding="utf-8", errors="replace"))
if count < MIN_WORDS:
thin.append((name, count))

if thin:
print(
f"{len(thin)} page(s) built with almost no content -- a snippet "
"include is probably resolving to nothing:",
file=sys.stderr,
)
for name, count in thin:
print(f" {name}: {count} words", file=sys.stderr)
return 1

print(f"ok {len(pages)} pages built, all carrying real content")
return 0


if __name__ == "__main__":
raise SystemExit(main())