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
2 changes: 2 additions & 0 deletions .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:

- name: CLI docs
run: make cli-docs
- name: Check nav is complete
run: make check-nav
- name: Build
run: APPPACK_VERSION=$(apppack version) make build
- uses: actions/upload-artifact@v5
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/public/
/src/command-line-reference/
__pycache__/
23 changes: 18 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
CLI_DOCS_DIR := src/command-line-reference

.PHONY: build
build:
SITE_URL=https://docs.apppack.io/ uv run zensical build
build: $(CLI_DOCS_DIR)
SITE_URL=https://docs.apppack.io/ uv run zensical build --strict

# The CLI reference pages are gitignored, so a fresh clone has none. Generate
# them when the directory is missing, but leave an existing one alone -- CI runs
# `make cli-docs` explicitly and this avoids a second docgen there. Run
# `make cli-docs` to force a refresh against a newer CLI.
$(CLI_DOCS_DIR):
$(MAKE) cli-docs

.PHONY: deploy
deploy:
Expand All @@ -14,9 +23,13 @@ clean:

.PHONY: cli-docs
cli-docs:
apppack docgen --directory src/command-line-reference
python3 scripts/generate_cli_nav.py
apppack docgen --directory $(CLI_DOCS_DIR)
uv run python scripts/generate_cli_nav.py

.PHONY: check-nav
check-nav:
uv run python scripts/check_nav_complete.py

.PHONY: run
run:
run: $(CLI_DOCS_DIR)
uv run zensical serve
5 changes: 5 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ markdown_extensions:
emoji_index: !!python/name:zensical.extensions.emoji.twemoji
emoji_generator: !!python/name:zensical.extensions.emoji.to_svg
- tables
# Zensical silently ignores plugins it does not implement -- it collects the
# names without validating them, so an unsupported plugin is a no-op rather
# than an error. That is how mkdocs-literate-nav failed quietly during the
# migration (the nav fell back to auto-generation). When upgrading Zensical,
# expect "the plugin does nothing" as the failure mode, not a build error.
plugins:
- search
- macros
Expand Down
86 changes: 86 additions & 0 deletions scripts/check_nav_complete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Verify every page under src/ is reachable from the nav in mkdocs.yml.

Zensical builds pages that are missing from `nav:` without complaining: they
get a URL and land in sitemap.xml and the search index, but nothing links to
them. Its config module is explicit that this will not be validated upstream
for now ("we only support validation of links right now, as navigation will
change significantly"), and the literate-nav directory globs that used to pick
up new pages automatically are gone, so we check it here instead.
"""

import re
import sys
from pathlib import Path

ROOT = Path(__file__).parent.parent
CONFIG = ROOT / "mkdocs.yml"
DOCS = ROOT / "src"

# mkdocs.yml carries `!!python/name:` tags, which a plain YAML safe-load
# rejects, so pull the nav block out textually instead of parsing the file.
NAV_START = re.compile(r"^nav:\s*$")
TOP_LEVEL_KEY = re.compile(r"^[^\s#]")
MD_PATH = re.compile(r"[\w./-]+\.md")


def nav_pages() -> set[str]:
lines = CONFIG.read_text(encoding="utf-8").splitlines()
try:
start = next(i for i, line in enumerate(lines) if NAV_START.match(line))
except StopIteration:
sys.exit(f"No `nav:` block found in {CONFIG}")

pages = set()
for line in lines[start + 1 :]:
# The nav block runs until the next top-level key.
if TOP_LEVEL_KEY.match(line):
break
pages.update(MD_PATH.findall(line))
return pages


def main() -> int:
referenced = nav_pages()
if not referenced:
print(f"No pages referenced in the `nav:` block of {CONFIG}", file=sys.stderr)
return 1

on_disk = {str(p.relative_to(DOCS)) for p in DOCS.rglob("*.md")}
missing = sorted(on_disk - referenced)
if missing:
print(
f"{len(missing)} page(s) exist under {DOCS.relative_to(ROOT)}/ but are "
f"not in the `nav:` block of {CONFIG.name}, so nothing links to them:",
file=sys.stderr,
)
for page in missing:
print(f" - {page}", file=sys.stderr)
print(
"\nAdd them to `nav:` in mkdocs.yml. Command line reference pages are "
"generated -- run `make cli-docs` instead of adding them by hand.",
file=sys.stderr,
)
return 1

stale = sorted(referenced - on_disk)
if stale:
print(
f"{len(stale)} page(s) are in the `nav:` block of {CONFIG.name} but do "
f"not exist on disk, so the nav links to them are dead:",
file=sys.stderr,
)
for page in stale:
print(f" - {page}", file=sys.stderr)
print(
"\nIf these are command line reference pages, run `make cli-docs`.",
file=sys.stderr,
)
return 1

print(f"All {len(on_disk)} pages under src/ are referenced in the nav.")
return 0


if __name__ == "__main__":
sys.exit(main())
4 changes: 2 additions & 2 deletions scripts/generate_cli_nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def main() -> int:
print(f"No pages found in {CLI_DOCS}. Run `apppack docgen` first.", file=sys.stderr)
return 1

lines = CONFIG.read_text().splitlines()
lines = CONFIG.read_text(encoding="utf-8").splitlines()
try:
begin = next(i for i, l in enumerate(lines) if l.strip() == BEGIN)
end = next(i for i, l in enumerate(lines) if l.strip() == END)
Expand All @@ -34,7 +34,7 @@ def main() -> int:
indent = " " * (len(lines[begin]) - len(lines[begin].lstrip()))
entries = [f"{indent}- command-line-reference/{name}" for name in pages]
lines[begin + 1 : end] = entries
CONFIG.write_text("\n".join(lines) + "\n")
CONFIG.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"Wrote {len(pages)} command line reference nav entries to {CONFIG}")
return 0

Expand Down
Loading