From 74864761fa152c609977214970d110983207ae75 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 12 May 2026 10:02:15 -0600 Subject: [PATCH] feat(mdbook): switch versioning from git tags to versions/ directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the git-tag-based snapshot model with frozen-directory snapshots. Each released spec version is its own subtree under ./versions/vX.Y.Z/ — a literal copy of ./src/ taken at release time. Why: tagging the whole repo conflated *spec content* with *site infrastructure* (book.toml, theme, picker JS, workflow, build scripts). Improving any of those required cherry-picking the change back to every old tag. With directory snapshots, content is frozen but every deploy renders every version with TODAY's infrastructure — site fixes propagate everywhere for free. Changes: - scripts/build-site.sh now iterates ./versions/v* directories instead of `git tag --list 'v*'`. Each per-version build sets MDBOOK_BOOK__SRC to the snapshot dir and MDBOOK_OUTPUT__HTML__SITE_URL to /meshcore-spec//. The root book.toml, theme, and picker JS apply uniformly. - justfile gains `cut-version vX.Y.Z` — validates the name, refuses to overwrite, runs `cp -R src versions/`, prints next- step hints. Cutting a release is now `just cut-version v0.1.0`, then a commit; optionally tag the commit for provenance, but tags no longer drive CI. - .github/workflows/mdbook.yml drops the `tags: ['v*']` trigger and `fetch-depth: 0` (no longer iterating git history). Triggers on push to main and PRs. - src/00-overview.md: "Spec Versions" section rewritten to describe the directory-snapshot model and the deliberate content-vs- infrastructure split. Smoke-tested locally: `just build-site` with no versions/ produces `_site/{latest/, versions.json=["latest"], index.html}`. `just cut-version v0.0.1` + rebuild produces both `latest/` and `v0.0.1/`, versions.json=`["latest","v0.0.1"]`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/mdbook.yml | 5 +-- justfile | 21 +++++++++++ scripts/build-site.sh | 71 +++++++++++++++++++----------------- src/00-overview.md | 12 +++++- 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/.github/workflows/mdbook.yml b/.github/workflows/mdbook.yml index a2dc7c7..906fde5 100644 --- a/.github/workflows/mdbook.yml +++ b/.github/workflows/mdbook.yml @@ -3,7 +3,6 @@ name: mdBook on: push: branches: [main] - tags: ['v*'] pull_request: branches: [main] workflow_dispatch: @@ -26,10 +25,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout (with tags) + - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Install mise uses: jdx/mise-action@v2 diff --git a/justfile b/justfile index 30a88fe..c716dfe 100644 --- a/justfile +++ b/justfile @@ -35,3 +35,24 @@ clean: # Run the corpus validator. validate: python3 tools/validate.py corpus/ + +# Cut a frozen spec version: copy ./src/ to ./versions//. +# Usage: just cut-version v0.2.0 +cut-version VERSION: + #!/usr/bin/env sh + set -eu + case "{{VERSION}}" in + v[0-9]*.[0-9]*.[0-9]*) ;; + *) echo "Version must look like vX.Y.Z (got '{{VERSION}}')" >&2; exit 1 ;; + esac + if [ -e "versions/{{VERSION}}" ]; then + echo "versions/{{VERSION}} already exists; not overwriting." >&2 + exit 1 + fi + mkdir -p versions + cp -R src "versions/{{VERSION}}" + echo "Snapshotted src/ -> versions/{{VERSION}}" + echo + echo "Next steps:" + echo " - git add versions/{{VERSION}} && commit (e.g. \"release {{VERSION}}\")" + echo " - optionally tag the commit: git tag {{VERSION}}" diff --git a/scripts/build-site.sh b/scripts/build-site.sh index 10dd600..aadf8fe 100755 --- a/scripts/build-site.sh +++ b/scripts/build-site.sh @@ -1,17 +1,19 @@ #!/usr/bin/env sh # Build the full multi-version site into ./_site/: -# - "latest" from the current HEAD -# - one subtree per v* git tag +# - "latest" from ./src/ +# - one subtree per ./versions/vX.Y.Z/ directory +# +# Each per-version build uses today's repo-root build infrastructure +# (book.toml, theme/, picker JS, scripts/), only swapping in the +# frozen content directory via MDBOOK_BOOK__SRC. This means every +# deploy gives every published version the current theme — site +# improvements never need backporting to old snapshots. # -# Each per-version build gets MDBOOK_OUTPUT__HTML__SITE_URL set so its -# canonical URL and sitemap are correct for the deployed location. # Emits _site/versions.json (newest first; consumed by -# theme/version-picker.js) and a root index.html that redirects to -# /latest/. +# theme/version-picker.js) and _site/index.html (redirect to /latest/). # -# Intended to be called via `just build-site`. SITE_BASE can be -# overridden to deploy under a different URL prefix; the default -# targets https://swaits.github.io/meshcore-spec/. +# Cut a new frozen version with `just cut-version vX.Y.Z`, which +# snapshots ./src/ into ./versions/vX.Y.Z/. set -eu @@ -19,7 +21,6 @@ SITE_BASE="${SITE_BASE:-/meshcore-spec}" WORKSPACE="$(pwd)" SITE_DIR="$WORKSPACE/_site" -# Section grouping for GitHub Actions logs; noop locally. group() { printf '::group::%s\n' "$1"; } endgroup() { printf '::endgroup::\n'; } warn() { printf '::warning::%s\n' "$1"; } @@ -27,43 +28,45 @@ warn() { printf '::warning::%s\n' "$1"; } rm -rf "$SITE_DIR" mkdir -p "$SITE_DIR" -# ---- latest (current HEAD) ---- +# ---- latest (from src/) ---- group "build latest" MDBOOK_OUTPUT__HTML__SITE_URL="${SITE_BASE}/latest/" \ mdbook build "$WORKSPACE" -d "$SITE_DIR/latest" endgroup -# ---- each v* tag ---- -# Each tag is built from its own git worktree so today's mdbook config -# / theme files don't bleed into older builds. -TAGS="$(git tag --list 'v*' --sort=-v:refname 2>/dev/null || true)" -for tag in $TAGS; do - [ -z "$tag" ] && continue - group "build $tag" - wt="$(mktemp -d -t mdbook-tag-XXXXXX)" - git worktree add --detach "$wt" "$tag" - if [ -f "$wt/book.toml" ]; then - if ! MDBOOK_OUTPUT__HTML__SITE_URL="${SITE_BASE}/${tag}/" \ - mdbook build "$wt" -d "$SITE_DIR/${tag}"; then - warn "mdbook build failed for ${tag}; skipping" - rm -rf "${SITE_DIR:?}/${tag:?}" - fi - else - warn "${tag} has no book.toml; skipping" +# ---- frozen versions (./versions/v*) ---- +# Discover and sort by semver (newest first). The list is captured +# into a variable so we can iterate it twice — once to build, once to +# emit versions.json — without re-scanning. +list_versions() { + for d in versions/v*; do + [ -d "$d" ] || continue + basename "$d" + done | sort -V -r +} +VERSION_DIRS="$(list_versions)" + +for version in $VERSION_DIRS; do + vdir="versions/$version" + [ -d "$vdir" ] || continue + group "build $version" + if ! MDBOOK_BOOK__SRC="$vdir" \ + MDBOOK_OUTPUT__HTML__SITE_URL="${SITE_BASE}/${version}/" \ + mdbook build "$WORKSPACE" -d "$SITE_DIR/${version}"; then + warn "mdbook build failed for ${version}; skipping" + rm -rf "${SITE_DIR:?}/${version:?}" fi - git worktree remove --force "$wt" endgroup done # ---- versions.json (consumed by theme/version-picker.js) ---- -# Newest first; "latest" always at the top. +# "latest" always first; then each successfully-built frozen version. { printf '[' printf '"latest"' - for tag in $TAGS; do - [ -z "$tag" ] && continue - [ -d "$SITE_DIR/$tag" ] || continue - printf ',"%s"' "$tag" + for version in $VERSION_DIRS; do + [ -d "$SITE_DIR/$version" ] || continue + printf ',"%s"' "$version" done printf ']\n' } > "$SITE_DIR/versions.json" diff --git a/src/00-overview.md b/src/00-overview.md index 6227acb..39294af 100644 --- a/src/00-overview.md +++ b/src/00-overview.md @@ -42,8 +42,16 @@ The published site preserves prior spec versions alongside the rolling - `https://swaits.github.io/meshcore-spec/latest/` — built from the tip of `main`. May change between any two visits. - `https://swaits.github.io/meshcore-spec/v0.1.0/` (and likewise for any - future `vX.Y.Z`) — a frozen snapshot built from the matching git tag. - Suitable for citing or for certifying an implementation against. + future `vX.Y.Z`) — a frozen content snapshot. Suitable for citing or for + certifying an implementation against. + +Frozen versions live as their own directory in the repo +(`versions/vX.Y.Z/`) — a literal copy of `src/` taken at release time. +This deliberately separates **content** (versioned, frozen) from **site +infrastructure** (theme, picker, build pipeline; lives at the repo root +and is unversioned). Every deploy rebuilds every published version using +today's infrastructure, so site improvements show up everywhere without +backporting. The version-picker in the top-right of every page lists every published version; switching keeps you on the same chapter where it exists in the