From eee192b87045082081d7c96e28328a02001868ee Mon Sep 17 00:00:00 2001 From: sehkone Date: Sat, 8 Aug 2026 15:55:42 +0900 Subject: [PATCH] Record the theme version on generated PDFs A PDF is handed to a reader and leaves the repository that produced it. Both places the installed version is recorded are deliberately kept out of the build -- .meta is dot-prefixed and MkDocs drops it, theme.toml is in exclude_docs -- so nothing in the artifact said which theme rendered it. A site is a git log away from that answer; a PDF is not, and once four projects bump the theme on their own schedules it stops being inferable at all. The cover now carries the line. It reads .meta rather than theme.toml because .meta records what is installed and theme.toml only what was requested, so a drifted tree cannot misreport itself. A --source install takes its version from theme.toml without ever resolving a release, so printing that number would assert something untrue. It renders an explicit local-build marker instead. A missing .meta is an error. A cover that quietly omits the line is the same class of silent asset loss the rest of this pipeline exists to prevent. Closes #33 --- CHANGELOG.md | 11 +++++++ README.md | 16 +++++++++ scripts/build-docs-pdf.sh | 39 ++++++++++++++++++++++ templates/api-reference/pdf/cover.html.j2 | 4 +++ templates/api-reference/pdf/styles.scss | 13 ++++++++ templates/manual/pdf/cover.html.j2 | 4 +++ templates/manual/pdf/styles.scss | 13 ++++++++ tests/pdf-test.sh | 40 +++++++++++++++++++++++ 8 files changed, 140 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4560e..9a92ffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,11 +26,17 @@ sections that affect its template. Version numbers follow the - `theme.favicon` points at `theme/brand-symbol.svg` rather than the wordmark. +- The PDF cover carries a line naming the theme that rendered it. A PDF + leaves the repository that produced it, so without it the artifact says + nothing about what to reproduce a rendering complaint against. ### api-reference - `theme.favicon` points at `theme/brand-symbol.svg` rather than the wordmark. +- The PDF cover carries a line naming the theme that rendered it. A PDF + leaves the repository that produced it, so without it the artifact says + nothing about what to reproduce a rendering complaint against. ### scripts @@ -40,6 +46,11 @@ sections that affect its template. Version numbers follow the project documenting a product with its own mark can put it on the cover. A path that does not resolve is an error rather than a silent fall back to the company mark. +- `build-docs-pdf.sh` reads `docs/theme/.meta` for the cover's provenance + line, so a drifted tree cannot misreport itself as a release. A + `--source` install renders `docs-theme (local build)` rather than the + version it was asked for, which never resolved a release. A missing + `.meta` is an error. ## [0.2.0] - 2026-08-08 diff --git a/README.md b/README.md index 6cf2013..7f3ed7b 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,22 @@ with more than one needs: It defaults to `mkdocs.yml`. Any other argument shape is a usage error. +#### Build Provenance + +Every cover carries a line naming the theme that rendered it — +`docs-theme 1.2.3` for a downloaded install. A PDF is handed to a reader +and leaves the repository that produced it, so without this there is +nothing in the artifact to reproduce a rendering complaint against, and +once several projects bump the theme on their own schedules the answer +stops being inferable. + +The value comes from `docs/theme/.meta`, the record of what is installed, +rather than from the request in `docs/theme.toml` — a hand-edited or +drifted tree would otherwise misreport itself. A `--source` install has a +version that never resolved a release, so it prints `docs-theme (local +build)` instead of a number. It is not configurable, and a missing +`.meta` is an error rather than a quietly omitted line. + #### Cover and Output Configuration All cover text and the output filename come from an `extra.pdf` block diff --git a/scripts/build-docs-pdf.sh b/scripts/build-docs-pdf.sh index 03b66a0..9671673 100755 --- a/scripts/build-docs-pdf.sh +++ b/scripts/build-docs-pdf.sh @@ -174,6 +174,39 @@ if not os.path.isdir(theme_dir): fail("docs/theme/ not found. Install the theme first:\n" " ./scripts/fetch-theme.sh") +# Which docs-theme rendered this file. A PDF is handed to a reader and +# leaves the repository that produced it, so unless the artifact says so +# there is nothing to reproduce a rendering complaint against -- and once +# several projects bump the theme on their own schedules, the answer +# stops being inferable. .meta is the record of what is installed; the +# requested value in docs/theme.toml would misreport a drifted tree. +meta_path = os.path.join(theme_dir, ".meta") +if not os.path.isfile(meta_path): + fail(f"{meta_path} not found. The installed theme is incomplete; " + "re-run ./scripts/fetch-theme.sh.") + +installed = {} +with open(meta_path, "r", encoding="utf-8") as f: + for line in f: + key, sep, value = line.partition("=") + if sep: + installed[key.strip()] = value.strip().strip('"') + +theme_version = installed.get("version") +theme_source = installed.get("source") +if not theme_version or not theme_source: + fail(f"{meta_path} records no version or source; re-run " + "./scripts/fetch-theme.sh.") + +# A --source install takes its version from docs/theme.toml without ever +# resolving a release, so printing that number would assert something +# untrue. Name the build instead. +if theme_source == "release": + theme_provenance = f"docs-theme {theme_version}" +else: + theme_provenance = ("docs-theme (local build)" if locale == "en" + else "docs-theme (로컬 빌드)") + theme_pdf = os.path.join(theme_dir, "pdf") if not os.path.isdir(theme_pdf): fail("docs/theme/pdf/ not found. The installed template ships no PDF " @@ -336,6 +369,12 @@ elif "cover_tagline" in extra: del extra["cover_tagline"] data["extra"] = extra +# theme_provenance reaches {{ theme_provenance }} in pdf/cover.html.j2 the +# same way, through `extra`. It is derived from the installed theme, not +# configured, so a consumer value is overwritten rather than honoured. +extra["theme_provenance"] = theme_provenance +data["extra"] = extra + if isinstance(plugins, dict): plugins["with-pdf"] = options elif isinstance(plugins, list): diff --git a/templates/api-reference/pdf/cover.html.j2 b/templates/api-reference/pdf/cover.html.j2 index 6c38c8d..0158715 100644 --- a/templates/api-reference/pdf/cover.html.j2 +++ b/templates/api-reference/pdf/cover.html.j2 @@ -31,5 +31,9 @@ {% endfor %} {% endif %} + + {% if theme_provenance %} +

{{ theme_provenance | e }}

+ {% endif %} diff --git a/templates/api-reference/pdf/styles.scss b/templates/api-reference/pdf/styles.scss index 5c721db..c21b1f8 100644 --- a/templates/api-reference/pdf/styles.scss +++ b/templates/api-reference/pdf/styles.scss @@ -197,6 +197,19 @@ p.meta { color: #71717B; } +/* Build provenance: which docs-theme rendered this file. A PDF leaves + * the repository that built it, so without this line there is nothing in + * the artifact to reproduce a rendering complaint against. */ +p.meta-provenance { + color: #B5B5BC; + font-size: 10px; + font-weight: 400; + line-height: 16px; + letter-spacing: -0.1px; + white-space: nowrap; + margin: 6px 0 0; +} + p.meta-copyright { color: #9F9FA9; font-size: 12px; diff --git a/templates/manual/pdf/cover.html.j2 b/templates/manual/pdf/cover.html.j2 index 6c38c8d..0158715 100644 --- a/templates/manual/pdf/cover.html.j2 +++ b/templates/manual/pdf/cover.html.j2 @@ -31,5 +31,9 @@ {% endfor %} {% endif %} + + {% if theme_provenance %} +

{{ theme_provenance | e }}

+ {% endif %} diff --git a/templates/manual/pdf/styles.scss b/templates/manual/pdf/styles.scss index 5c721db..c21b1f8 100644 --- a/templates/manual/pdf/styles.scss +++ b/templates/manual/pdf/styles.scss @@ -197,6 +197,19 @@ p.meta { color: #71717B; } +/* Build provenance: which docs-theme rendered this file. A PDF leaves + * the repository that built it, so without this line there is nothing in + * the artifact to reproduce a rendering complaint against. */ +p.meta-provenance { + color: #B5B5BC; + font-size: 10px; + font-weight: 400; + line-height: 16px; + letter-spacing: -0.1px; + white-space: nowrap; + margin: 6px 0 0; +} + p.meta-copyright { color: #9F9FA9; font-size: 12px; diff --git a/tests/pdf-test.sh b/tests/pdf-test.sh index e20c349..9ecf771 100755 --- a/tests/pdf-test.sh +++ b/tests/pdf-test.sh @@ -777,4 +777,44 @@ grep -q "brand-print.svg" "$WORK/no-print-logo.log" \ || die "the error does not name the missing asset" ok "a missing brand-print.svg fails instead of rendering a logo-less cover" +# --- build provenance on the cover -------------------------------------- + +# A PDF leaves the repository that built it, so the cover records which +# docs-theme rendered it. The fixtures install with --source, so this is +# the local form: naming a version that never resolved a release would +# assert something untrue. +prov="$WORK/prov" +new_project "$prov" +printf 'site_name: Provenance Fixture\n' > "$prov/mkdocs.yml" +build_pdf "$prov" en +build_pdf "$prov" ko +pdf_text "$prov/site/pdf/provenance-fixture.en.pdf" > "$WORK/prov-en.txt" +pdf_text "$prov/site/pdf/provenance-fixture.ko.pdf" > "$WORK/prov-ko.txt" +contains "$WORK/prov-en.txt" "docs-theme(localbuild)" "the en cover" +contains "$WORK/prov-ko.txt" "docs-theme(로컬빌드)" "the ko cover" +excludes "$WORK/prov-en.txt" "docs-theme1.2.3" "the en cover" +ok "a --source build names itself rather than claiming a release version" + +# A downloaded install prints the version .meta recorded. The value is +# read from .meta and not from docs/theme.toml, so a drifted tree cannot +# misreport itself. +sed -i.bak 's/^source = .*/source = "release"/' "$prov/docs/theme/.meta" +rm -f "$prov/docs/theme/.meta.bak" +build_pdf "$prov" en +pdf_text "$prov/site/pdf/provenance-fixture.en.pdf" > "$WORK/prov-release.txt" +contains "$WORK/prov-release.txt" "docs-theme1.2.3" "the cover of a release build" +excludes "$WORK/prov-release.txt" "localbuild" "the cover of a release build" +ok "a release install prints the version recorded in .meta" + +# Without .meta there is no provenance to print, and a cover that quietly +# omits it is the silent asset loss this whole check exists to prevent. +rm -f "$prov/docs/theme/.meta" +if (cd "$prov" && ./docs/theme/build-docs-pdf.sh en) > "$WORK/no-meta.log" 2>&1 +then + die "a missing .meta should be an error" +fi +grep -q "\.meta" "$WORK/no-meta.log" \ + || die "the error does not name .meta" +ok "a missing .meta fails instead of dropping the provenance line" + echo "All PDF script checks passed."