feat(mdbook): switch versioning from git tags to versions/ directories

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/<version>/. 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/<version>`, 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) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.7 committed 2026-05-12 10:03:46 -06:00
1 parent da00e46570
commit 74864761fa
4 files changed
+69 -40

No files matched your search

+1 -4
View File
@@ -3,7 +3,6 @@ name: mdBook
on: on:
push: push:
branches: [main] branches: [main]
tags: ['v*']
pull_request: pull_request:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
@@ -26,10 +25,8 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout (with tags) - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install mise - name: Install mise
uses: jdx/mise-action@v2 uses: jdx/mise-action@v2
+21
View File
@@ -35,3 +35,24 @@ clean:
# Run the corpus validator. # Run the corpus validator.
validate: validate:
python3 tools/validate.py corpus/ python3 tools/validate.py corpus/
# Cut a frozen spec version: copy ./src/ to ./versions/<VERSION>/.
# 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}}"
+37 -34
View File
@@ -1,17 +1,19 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Build the full multi-version site into ./_site/: # Build the full multi-version site into ./_site/:
# - "latest" from the current HEAD # - "latest" from ./src/
# - one subtree per v* git tag # - 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 # Emits _site/versions.json (newest first; consumed by
# theme/version-picker.js) and a root index.html that redirects to # theme/version-picker.js) and _site/index.html (redirect to /latest/).
# /latest/.
# #
# Intended to be called via `just build-site`. SITE_BASE can be # Cut a new frozen version with `just cut-version vX.Y.Z`, which
# overridden to deploy under a different URL prefix; the default # snapshots ./src/ into ./versions/vX.Y.Z/.
# targets https://swaits.github.io/meshcore-spec/.
set -eu set -eu
@@ -19,7 +21,6 @@ SITE_BASE="${SITE_BASE:-/meshcore-spec}"
WORKSPACE="$(pwd)" WORKSPACE="$(pwd)"
SITE_DIR="$WORKSPACE/_site" SITE_DIR="$WORKSPACE/_site"
# Section grouping for GitHub Actions logs; noop locally.
group() { printf '::group::%s\n' "$1"; } group() { printf '::group::%s\n' "$1"; }
endgroup() { printf '::endgroup::\n'; } endgroup() { printf '::endgroup::\n'; }
warn() { printf '::warning::%s\n' "$1"; } warn() { printf '::warning::%s\n' "$1"; }
@@ -27,43 +28,45 @@ warn() { printf '::warning::%s\n' "$1"; }
rm -rf "$SITE_DIR" rm -rf "$SITE_DIR"
mkdir -p "$SITE_DIR" mkdir -p "$SITE_DIR"
# ---- latest (current HEAD) ---- # ---- latest (from src/) ----
group "build latest" group "build latest"
MDBOOK_OUTPUT__HTML__SITE_URL="${SITE_BASE}/latest/" \ MDBOOK_OUTPUT__HTML__SITE_URL="${SITE_BASE}/latest/" \
mdbook build "$WORKSPACE" -d "$SITE_DIR/latest" mdbook build "$WORKSPACE" -d "$SITE_DIR/latest"
endgroup endgroup
# ---- each v* tag ---- # ---- frozen versions (./versions/v*) ----
# Each tag is built from its own git worktree so today's mdbook config # Discover and sort by semver (newest first). The list is captured
# / theme files don't bleed into older builds. # into a variable so we can iterate it twice — once to build, once to
TAGS="$(git tag --list 'v*' --sort=-v:refname 2>/dev/null || true)" # emit versions.json — without re-scanning.
for tag in $TAGS; do list_versions() {
[ -z "$tag" ] && continue for d in versions/v*; do
group "build $tag" [ -d "$d" ] || continue
wt="$(mktemp -d -t mdbook-tag-XXXXXX)" basename "$d"
git worktree add --detach "$wt" "$tag" done | sort -V -r
if [ -f "$wt/book.toml" ]; then }
if ! MDBOOK_OUTPUT__HTML__SITE_URL="${SITE_BASE}/${tag}/" \ VERSION_DIRS="$(list_versions)"
mdbook build "$wt" -d "$SITE_DIR/${tag}"; then
warn "mdbook build failed for ${tag}; skipping" for version in $VERSION_DIRS; do
rm -rf "${SITE_DIR:?}/${tag:?}" vdir="versions/$version"
fi [ -d "$vdir" ] || continue
else group "build $version"
warn "${tag} has no book.toml; skipping" 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 fi
git worktree remove --force "$wt"
endgroup endgroup
done done
# ---- versions.json (consumed by theme/version-picker.js) ---- # ---- 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 '['
printf '"latest"' printf '"latest"'
for tag in $TAGS; do for version in $VERSION_DIRS; do
[ -z "$tag" ] && continue [ -d "$SITE_DIR/$version" ] || continue
[ -d "$SITE_DIR/$tag" ] || continue printf ',"%s"' "$version"
printf ',"%s"' "$tag"
done done
printf ']\n' printf ']\n'
} > "$SITE_DIR/versions.json" } > "$SITE_DIR/versions.json"
+10 -2
View File
@@ -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 - `https://swaits.github.io/meshcore-spec/latest/` — built from the tip of
`main`. May change between any two visits. `main`. May change between any two visits.
- `https://swaits.github.io/meshcore-spec/v0.1.0/` (and likewise for any - `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. future `vX.Y.Z`) — a frozen content snapshot. Suitable for citing or for
Suitable for citing or for certifying an implementation against. 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 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 version; switching keeps you on the same chapter where it exists in the