diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81c539d..5088911 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,22 @@ jobs: fi done + - name: Verify theme delivery in built sites + run: | + # --strict fails on broken links, not on a stylesheet or font + # that 404s. Assert the theme's assets actually reached each + # built site/ directory, reading extra_css and theme.logo from + # the resolved config so the check tracks mkdocs-base.yml. + for sample_dir in samples/*/; do + name="$(basename "$sample_dir")" + if [ -f "$sample_dir/mkdocs.yml" ]; then + echo "Verifying theme delivery for $name..." + python3 scripts/verify-theme-delivery.py \ + --config "$sample_dir/mkdocs.yml" \ + --site "site-ci/$name" + fi + done + build-pdf: name: Build PDF runs-on: ubuntu-latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f603b5d..f274546 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,6 +39,15 @@ jobs: run: | mkdir -p site cp samples/index.html site/ + # This site deploys on every push to main, so it shows + # unreleased state no consumer can yet install. Stamp the built + # revision onto the landing page so an evaluator is not misled + # about what they are looking at. + SHORT_SHA="${GITHUB_SHA:0:7}" + COMMIT_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}" + sed -i \ + "s|Preview build from the local working tree|Built from ${SHORT_SHA} on $(date -u +%Y-%m-%d)|" \ + site/index.html for sample_dir in samples/*/; do name="$(basename "$sample_dir")" if [ -f "$sample_dir/mkdocs.yml" ]; then diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46a7788..76cde65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,11 +15,22 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7 + with: + # The surface guard reads the previous tag's tree, so + # the job needs full history and the tags to resolve and + # read it. actions/checkout is shallow and tagless by + # default, which would let the guard pass while silently + # comparing against nothing. + fetch-depth: 0 + fetch-tags: true - name: Extract version from tag id: version run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + - name: Guard release surface + run: ./scripts/check-release-surface.sh "${{ steps.version.outputs.version }}" + - name: Extract release notes from CHANGELOG.md id: release_notes run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e11b3..dc975de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,72 +1,90 @@ # Changelog -This file documents recent notable changes to this project. The format of this -file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and -this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +This file documents recent notable changes to this project. Within each +release, entries are grouped by the area a consumer's reviewer triages — +`manual`, `api-reference`, `shared`, and `scripts` — rather than by the +Keep a Changelog change-type headings, so a consumer reads only the +sections that affect its template. Version numbers follow the +`MAJOR.MINOR.PATCH` policy described in [the README](README.md#versioning). ## Unreleased -### Added +### manual -- Both `mkdocs-base.yml` files now carry a self-served browser identity: - an indigo palette with a light/dark toggle, `brand.svg` as the header - logo and favicon, and `theme.font: false` so no Google Fonts are - requested. `shared/styles/base.css` declares `@font-face` for the six - shipped woff2 files and sets `--md-text-font: "Pretendard"`, giving - Korean body typography served entirely from the site. -- The base `markdown_extensions` gained a Unicode-aware `toc.slugify`, - so Korean headings keep their characters in anchor ids, and a - `mermaid` custom fence under `pymdownx.superfences`, so a tagged block - renders as a `.mermaid` diagram. `base.css` also carries shared rules - that scroll wide tables horizontally and center Mermaid diagrams. -- Added `scripts/install-samples.sh`, which installs the theme into each - sample from the local checkout so the samples build through the same - `INHERIT` path a consumer uses. It takes a `--force` (`--clean`) flag - that drops each sample's generated `docs/theme/` before reinstalling, - so edits to a template or shared file are picked up instead of the - cached tree `fetch-theme.sh` would otherwise keep. `serve-samples.sh` - passes `--force` on every run so the preview always reflects edits. +- `mkdocs-base.yml` now carries a self-served browser identity: an indigo + palette with a light/dark toggle, `brand.svg` as the header logo and + favicon, and `theme.font: false` so no Google Fonts are requested. +- The base `markdown_extensions` gained a Unicode-aware `toc.slugify`, so + Korean headings keep their characters in anchor ids, and a `mermaid` + custom fence under `pymdownx.superfences`, so a tagged block renders as + a `.mermaid` diagram. +- Consuming projects now `INHERIT: docs/theme/mkdocs-base.yml` instead of + copying settings out of it. Because MkDocs replaces lists and scalars + wholesale on merge, `theme.features`, `markdown_extensions`, + `extra_css`, and `exclude_docs` are defined fully in the base and must + never be partially overridden by a consumer. The sample builds through + this path against a theme installed from the local checkout; its + `docs/theme/` tree is generated, not committed. +- `extra_css` now points at `theme/`. -### Changed +### api-reference -- Consuming projects now `INHERIT: docs/theme/mkdocs-base.yml` instead - of copying settings out of it. The base is the union of what every - consumer needs; because MkDocs replaces lists and scalars wholesale on - merge, `theme.features`, `markdown_extensions`, `extra_css`, and - `exclude_docs` must be defined fully in the base and never partially - overridden by a consumer. Both sample sites build through this path - against a theme installed from the local checkout; a sample's +- `mkdocs-base.yml` now carries a self-served browser identity: an indigo + palette with a light/dark toggle, `brand.svg` as the header logo and + favicon, and `theme.font: false` so no Google Fonts are requested. +- The base `markdown_extensions` gained a Unicode-aware `toc.slugify`, so + Korean headings keep their characters in anchor ids, and a `mermaid` + custom fence under `pymdownx.superfences`, so a tagged block renders as + a `.mermaid` diagram. +- Consuming projects now `INHERIT: docs/theme/mkdocs-base.yml` instead of + copying settings out of it. Because MkDocs replaces lists and scalars + wholesale on merge, `theme.features`, `markdown_extensions`, + `extra_css`, and `exclude_docs` are defined fully in the base and must + never be partially overridden by a consumer. The sample builds through + this path against a theme installed from the local checkout; its `docs/theme/` tree is generated, not committed. +- `extra_css` now points at `theme/` and includes `api.css`. + +### shared + +- `styles/base.css` declares `@font-face` for the six shipped woff2 files + and sets `--md-text-font: "Pretendard"`, giving Korean body typography + served entirely from the site. It also carries shared rules that scroll + wide tables horizontally and center Mermaid diagrams. +### scripts + +- Added `install-samples.sh`, which installs the theme into each sample + from the local checkout so the samples build through the same `INHERIT` + path a consumer uses. It takes a `--force` (`--clean`) flag that drops + each sample's generated `docs/theme/` before reinstalling, so edits to a + template or shared file are picked up instead of the cached tree + `fetch-theme.sh` would otherwise keep. `serve-samples.sh` passes + `--force` on every run so the preview always reflects edits. - `fetch-theme.sh` now installs into `docs/theme/` instead of - `docs/.theme/`. MkDocs excludes every dot-prefixed path from the - build, so assets installed under the old path never reached the - published site. + `docs/.theme/`. MkDocs excludes every dot-prefixed path from the build, + so assets installed under the old path never reached the published site. - `fetch-theme.sh` takes no arguments. The repository, template, and version are read from `docs/theme.toml`, which the consuming project commits. `--source ` installs from a local checkout instead of a release and records `source = "local"` in `docs/theme/.meta`. -- The install set gained `mkdocs-base.yml`, `shared/styles/base.css`, - and `build-docs-pdf.sh`, so consumers no longer keep their own copy - of the PDF script. `docs/theme/.meta` records the installed `repo`, - `version`, `template`, `digest`, and `source`; an unchanged tree is - left alone and an edited one is reinstalled. `docs/theme/` is the only - path the installer replaces: the tree is staged in a uniquely named - directory created beside it, so nothing else the project keeps under - `docs/` is removed. -- `build-docs-pdf.sh` takes an optional config path after the locale - and reads all cover text and the output filename from the config's +- The install set gained `mkdocs-base.yml`, `shared/styles/base.css`, and + `build-docs-pdf.sh`, so consumers no longer keep their own copy of the + PDF script. `docs/theme/.meta` records the installed `repo`, `version`, + `template`, `digest`, and `source`; an unchanged tree is left alone and + an edited one is reinstalled. `docs/theme/` is the only path the + installer replaces: the tree is staged in a uniquely named directory + created beside it, so nothing else the project keeps under `docs/` is + removed. +- `build-docs-pdf.sh` takes an optional config path after the locale and + reads all cover text and the output filename from the config's `extra.pdf` block. `extra.pdf_copyright` is replaced by - `extra.pdf.copyright` and is now an error. The config it generates - for MkDocs goes to a uniquely named scratch file, so a project that - already has an `mkdocs.tmp.yml` no longer has it overwritten and - deleted. `extra.pdf` is the only source of cover text: a leftover - top-level `extra.cover_tagline` no longer renders when - `extra.pdf.cover_tagline` is unset for the locale being built. -- `extra_css` in both `mkdocs-base.yml` files points at `theme/`. - -### Removed - + `extra.pdf.copyright` and is now an error. The config it generates for + MkDocs goes to a uniquely named scratch file, so a project that already + has an `mkdocs.tmp.yml` no longer has it overwritten and deleted. + `extra.pdf` is the only source of cover text: a leftover top-level + `extra.cover_tagline` no longer renders when `extra.pdf.cover_tagline` + is unset for the locale being built. - Removed the root `VERSION` file. The git tag is the version. ## [0.1.0] - 2026-03-30 diff --git a/README.md b/README.md index d2d6781..1bc7551 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,31 @@ from the tag and extracts release notes from `CHANGELOG.md`. `fetch-theme.sh` uses `gh release download`, so the release must exist for consumers to install a version. +## Versioning + +Releases are tagged `MAJOR.MINOR.PATCH` with no `v` prefix, matching the +tag pattern (`[0-9]+.[0-9]+.[0-9]+`) that `release.yml` triggers on. +Because a single commit here controls every consumer's build and a tag +becomes a version-bump pull request in each consumer, the number states +how a release can affect a consumer's build: + +- **MAJOR** — removing a markdown extension or theme feature from + `mkdocs-base.yml`, renaming or relocating an installed path, or any + other change that can break a consumer's build. Consumers must review + these against their own documents. +- **MINOR** — adding a markdown extension, theme feature, or asset. +- **PATCH** — CSS and PDF adjustments that cannot break a build. + +Templates are **not** versioned separately: one repository version covers +all of them, and the vendored diff in a consumer's bump pull request +shows whether that consumer is actually affected. + +A release is only meaningful when the installed surface actually changed. +The release workflow rejects a tag whose release surface — everything +`fetch-theme.sh` installs, plus `fetch-theme.sh` itself — is byte-identical +to the previous tag's, because such a tag has nothing for consumers to +fetch and would only produce an empty-diff bump pull request. + ## For Consuming Projects ### Initial Setup @@ -167,6 +192,20 @@ staged first in a uniquely named directory created beside it, which is removed when the run ends, so an interrupted install leaves neither a half-written `docs/theme/` nor anything else you keep under `docs/`. +**Commit the installed `docs/theme/` tree.** It is vendored, not +git-ignored: committing it makes the theme part of your repository, so +the site builds reproducibly for anyone who checks the project out and a +version bump lands as a reviewable diff. + +The installer writes `docs/theme/.meta`, which records a `digest` of the +installed files. Verify the committed tree against that digest by +re-running the installer: a run whose `.meta` agrees with +`docs/theme.toml` and whose digest still matches the files on disk exits +without changing anything, while any edit, addition, or deletion under +`docs/theme/` is detected and reinstalled. Running `./scripts/fetch-theme.sh` +in CI is a convenient way to catch a vendored tree that has drifted from +its `.meta`. + ### Wiring mkdocs.yml Inherit the installed base config rather than copying settings out of @@ -334,9 +373,10 @@ the path of the generated config is then reported on stderr. ## GitHub Pages -On merge to `main`, the CI workflow builds all sample sites and -deploys them to GitHub Pages. The landing page links to each -template sample. +On merge to `main`, the Docs workflow builds all sample sites and +deploys them to GitHub Pages. The landing page links to each template +sample and is stamped with the commit it was built from, since the +deployed site shows unreleased state that no consumer can yet install. ## License diff --git a/samples/index.html b/samples/index.html index c8ca9e8..31977f0 100644 --- a/samples/index.html +++ b/samples/index.html @@ -71,6 +71,17 @@ text-decoration: none; } .card .links a:hover { text-decoration: underline; } + footer.provenance { + margin-top: 48px; + color: #9ca3af; + font-size: 0.8rem; + text-align: center; + } + footer.provenance a { + color: #6b7280; + text-decoration: none; + } + footer.provenance a:hover { text-decoration: underline; } @@ -96,5 +107,11 @@

Manual

+ + diff --git a/scripts/check-release-surface.sh b/scripts/check-release-surface.sh new file mode 100755 index 0000000..1ffbc25 --- /dev/null +++ b/scripts/check-release-surface.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Guard against cutting a release that consumers cannot fetch anything from. +# +# The "release surface" is exactly what fetch-theme.sh installs into a +# consumer, plus fetch-theme.sh itself, which a consumer copies. A tag +# whose surface is byte-identical to the previous tag's produces an +# empty-diff version-bump pull request in every consumer, so this guard +# rejects it. samples/, .github/, README.md, CHANGELOG.md, and lint +# configuration are repo-internal and are not part of the surface. +# +# Usage: +# ./scripts/check-release-surface.sh +# +# The previous tag is resolved by MAJOR.MINOR.PATCH ordering over the +# repo's release tags (the release.yml pattern [0-9]+.[0-9]+.[0-9]+). +# When no predecessor exists -- the first tag ever, or a tag that sorts +# below every existing one -- the guard passes without comparing. +# +# Requires the repository history and tags to be present: run after an +# actions/checkout with fetch-depth: 0 and fetch-tags: true, otherwise +# the previous tag cannot resolve or be read. +set -euo pipefail + +NEW_TAG="${1:?usage: check-release-surface.sh }" + +# Paths fetch-theme.sh installs into a consumer, plus fetch-theme.sh +# itself. templates/ is compared whole so all four templates are covered +# by byte-diff, even the ones no sample exercises. +SURFACE_PATHS=( + templates + shared/styles/base.css + shared/fonts + shared/brand.svg + scripts/build-docs-pdf.sh + scripts/fetch-theme.sh +) + +# All release tags in ascending MAJOR.MINOR.PATCH order. +tags="$(git tag --list | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V || true)" + +# The predecessor is the tag immediately before NEW_TAG in that order. +# grep -B1 prints the line before the match together with the match; the +# first line of that is the predecessor. If NEW_TAG is the first (or the +# only) release tag, that first line is NEW_TAG itself -- no predecessor. +prev_tag="$(printf '%s\n' "$tags" | grep -B1 -Fx "$NEW_TAG" | head -n1 || true)" + +if [ -z "$prev_tag" ] || [ "$prev_tag" = "$NEW_TAG" ]; then + echo "No predecessor tag for $NEW_TAG; skipping release-surface comparison." + exit 0 +fi + +echo "Comparing release surface: $prev_tag -> $NEW_TAG" + +if git diff --quiet "$prev_tag" "$NEW_TAG" -- "${SURFACE_PATHS[@]}"; then + cat >&2 </styles/base.css with the + # woff2 files alongside at <...>/fonts/. + base_css = next( + (rel for rel in extra_css if os.path.basename(rel) == "base.css"), + None) + if base_css is None: + failures.append("base.css was not found in extra_css, so the font " + "faces it declares cannot be located") + else: + fonts_rel = os.path.join(os.path.dirname(os.path.dirname(base_css)), + "fonts") + for face in FONT_FACES: + target = os.path.join(site, fonts_rel, face) + if os.path.isfile(target): + print(f" ok font {os.path.join(fonts_rel, face)}") + else: + failures.append(f"font face '{face}' is missing from the " + f"built site ({target})") + + # Excluded paths must NOT reach a consumer site. + for rel in EXCLUDED_PATHS: + target = os.path.join(site, rel.rstrip("/")) + if os.path.exists(target): + failures.append(f"excluded path '{rel}' reached the built site " + f"({target}); it must be kept out") + else: + print(f" ok excluded {rel} absent") + + if failures: + print(f"\nverify-theme-delivery: FAILED for {args.config}", + file=sys.stderr) + for message in failures: + print(f" - {message}", file=sys.stderr) + sys.exit(1) + + print(f"verify-theme-delivery: OK for {args.config}") + + +if __name__ == "__main__": + main()