From b3b4f111d3bddb9999da69e3034dd03227a342e0 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 26 Jun 2026 13:30:19 +0000 Subject: [PATCH 1/3] docs: inject the latest git tag into the tutorial snippets The tutorial's copy-paste blocks pinned this project at a literal `` placeholder readers had to fix by hand. Replace those with a `__LATEST_TAG__` sentinel and add a build-only MyST transform (docs/_ext/inject-latest-tag.mjs) that swaps it for the repo's latest tag (`git describe --tags --abbrev=0`) at build time, so the rendered snippets carry a real, current ref. The tag comes from a LATEST_TAG env set by the `docs`/`docs-dev` npm scripts (which fetch tags first so it works in a shallow CI checkout); the transform falls back to `git describe` directly and warns rather than silently shipping the sentinel if no tag resolves. Scope: tutorial only; reference/architecture keep the literal ``. Co-Authored-By: Claude Opus 4.8 --- docs/_ext/inject-latest-tag.mjs | 70 ++++++++++++++++++++++++ docs/myst.yml | 2 + docs/tutorials/adding-to-a-fresh-repo.md | 19 ++++--- package.json | 5 +- 4 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 docs/_ext/inject-latest-tag.mjs diff --git a/docs/_ext/inject-latest-tag.mjs b/docs/_ext/inject-latest-tag.mjs new file mode 100644 index 0000000..343bee8 --- /dev/null +++ b/docs/_ext/inject-latest-tag.mjs @@ -0,0 +1,70 @@ +/** + * inject-latest-tag — a build-only MyST transform for *this repo's own docs*. + * + * The tutorial's copy-paste snippets pin this project at a release tag. Rather + * than hard-coding (and forever bumping) a version, the source carries the + * sentinel `__LATEST_TAG__` and this transform swaps it for the repo's latest + * tag at build time, so a reader copying the blocks gets a real, current ref. + * + * Unlike `plugins/version-switcher.mjs` (which is isomorphic — it also runs in + * the browser), this file only ever runs under MyST (Node) at build time, so it + * is free to touch git. It is loaded from `docs/myst.yml`, not shipped. + * + * The tag is taken from `LATEST_TAG` (set by the `docs`/`docs-dev` npm scripts, + * which also handle fetching tags in a shallow CI checkout); if that is unset we + * fall back to `git describe` directly (the bare `myst build` case). When no tag + * can be resolved the sentinel is left in place and a warning is emitted, so a + * missing tag is loud rather than silently shipping `__LATEST_TAG__` to readers. + */ +import { execSync } from "node:child_process"; + +const SENTINEL = "__LATEST_TAG__"; + +/** The latest release tag: `LATEST_TAG` env, else `git describe`, else null. */ +function resolveLatestTag() { + const fromEnv = process.env.LATEST_TAG?.trim(); + if (fromEnv) return fromEnv; + try { + // No fetch here — the npm scripts own that. This just reads what's local. + return execSync("git describe --tags --abbrev=0", { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return null; + } +} + +/** Replace the sentinel in every string `value` in the tree (text + code nodes). */ +function replaceInTree(node, tag) { + if (!node || typeof node !== "object") return; + if (typeof node.value === "string" && node.value.includes(SENTINEL)) { + node.value = node.value.split(SENTINEL).join(tag); + } + if (Array.isArray(node.children)) { + for (const child of node.children) replaceInTree(child, tag); + } +} + +const injectLatestTag = { + name: "inject-latest-tag", + doc: "Replace __LATEST_TAG__ with this repo's latest git tag at build time.", + stage: "document", + plugin: () => (tree, vfile) => { + const tag = resolveLatestTag(); + if (!tag) { + vfile?.message?.( + `inject-latest-tag: no tag resolved (set LATEST_TAG or fetch git tags); leaving ${SENTINEL} as-is.`, + ); + return; + } + replaceInTree(tree, tag); + }, +}; + +const plugin = { + name: "inject-latest-tag", + transforms: [injectLatestTag], +}; + +export default plugin; diff --git a/docs/myst.yml b/docs/myst.yml index daa1895..0efe995 100644 --- a/docs/myst.yml +++ b/docs/myst.yml @@ -6,6 +6,8 @@ project: # makes MyST localize this same file as the anywidget runtime. plugins: - ../plugins/version-switcher.mjs + # Build-only: swaps __LATEST_TAG__ in the tutorial for this repo's latest tag. + - ./_ext/inject-latest-tag.mjs toc: - file: index.md - file: tutorials.md diff --git a/docs/tutorials/adding-to-a-fresh-repo.md b/docs/tutorials/adding-to-a-fresh-repo.md index 3e6503f..3f3586a 100644 --- a/docs/tutorials/adding-to-a-fresh-repo.md +++ b/docs/tutorials/adding-to-a-fresh-repo.md @@ -8,8 +8,10 @@ version switcher and a versioned GitHub Pages deployment. By the end you will ha - a `stable/` alias pointing at your latest release. It assumes a repo that already builds docs with `myst build --html` from a `docs/` -directory. Replace `ORG/REPO` throughout, and pin a real `` from this project's -[releases](https://github.com/DiamondLightSource/myst-version-switcher-plugin/releases). +directory. Replace `ORG/REPO` throughout. The snippets below are pinned to the +latest release (`__LATEST_TAG__`); bump that pin to any version from this project's +[releases](https://github.com/DiamondLightSource/myst-version-switcher-plugin/releases) +when you need a different one. ## 1. Add the plugin to your MyST project @@ -19,7 +21,7 @@ In `docs/myst.yml`, load the plugin from its release asset and route a navbar pa # docs/myst.yml project: plugins: - - https://github.com/DiamondLightSource/myst-version-switcher-plugin/releases/download//version-switcher.mjs + - https://github.com/DiamondLightSource/myst-version-switcher-plugin/releases/download/__LATEST_TAG__/version-switcher.mjs site: template: book-theme parts: @@ -79,7 +81,7 @@ on: jobs: docs: # Call the docs building workflow directly - uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/docs.yml@ + uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/docs.yml@__LATEST_TAG__ with: # Whatever turns your sources into docs/_build/html at $BASE_URL. # uv and Node are preinstalled, so this can be make / tox / npx / npm. @@ -88,7 +90,7 @@ jobs: release: needs: [docs] if: github.ref_type == 'tag' # tag pushes only - uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/release.yml@ + uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/release.yml@__LATEST_TAG__ permissions: contents: write # create the Release + attach assets @@ -109,10 +111,11 @@ jobs: ### `publish-dispatch.yml` `publish.yml` is the engine and owns all the branching; this is a thin **shim** — the -only thing that calls it, and the one place you pin `@`. It has to exist as a file +only thing that calls it, and the one place you pin `@__LATEST_TAG__`. It has to exist as a file in your repo (not just be `uses:`'d) because the tag trampoline re-dispatches it as a `workflow_dispatch`, and a reusable workflow can't be dispatched cross-repo. Copy it -verbatim, changing only ``: +verbatim; the only thing to keep current is the `publish.yml` pin, already set to +`__LATEST_TAG__`: ```yaml # .github/workflows/publish-dispatch.yml @@ -126,7 +129,7 @@ on: pr: { description: "Fork PR to approve + preview (empty = re-deploy)", required: false, default: "" } jobs: publish: - uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish.yml@ + uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish.yml@__LATEST_TAG__ with: version-name: ${{ inputs.version-name }} # "" on dispatch → pure durable gather pr: ${{ inputs.pr }} # set (dispatch) → pin that fork head SHA diff --git a/package.json b/package.json index 9835b32..a3ddc52 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ }, "scripts": { "test": "node test/test-url-logic.mjs && node test/test-assemble.mjs", - "docs": "cd docs && myst build --html", - "docs-dev": "cd docs && myst start" + "latest-tag": "git describe --tags --abbrev=0 2>/dev/null || (git fetch --tags --quiet 2>/dev/null && git describe --tags --abbrev=0)", + "docs": "LATEST_TAG=\"$(npm run -s latest-tag || true)\" sh -c 'cd docs && myst build --html'", + "docs-dev": "LATEST_TAG=\"$(npm run -s latest-tag || true)\" sh -c 'cd docs && myst start'" }, "files": [ "plugins/version-switcher.mjs", From 36b4273a4969d7111189b8030883eab964296595 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 26 Jun 2026 13:38:24 +0000 Subject: [PATCH 2/3] ci: checkout with fetch-depth 0 in docs.yml so tags are available The shallow default clone has no tags, so a build-command deriving a version from `git describe` (this repo's docs, to pin the tutorial snippets at the latest release) found none in CI and fell back to the sentinel. Fetch full history + tags unconditionally; tags are cheap and any consumer's build-command can now rely on them. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/docs.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c919c78..2ad69e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,7 +46,14 @@ jobs: outputs: version-name: ${{ steps.ver.outputs.version-name }} steps: + # fetch-depth: 0 brings the full history *and tags* — a build-command may + # derive a version from `git describe` (this repo's docs do, to pin the + # tutorial's snippets at the latest release), and the default shallow clone + # has no tags. Tags are cheap; fetch them unconditionally so any consumer's + # build-command can rely on them. - uses: actions/checkout@v7 + with: + fetch-depth: 0 # uv is installed unconditionally (Node/npx is already on the runner) so every # docs build has the same baseline env regardless of build-command. With no From fb74f7002a0bff0c755870037ceacb240683764e Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 26 Jun 2026 13:42:18 +0000 Subject: [PATCH 3/3] docs: simplify tag injection to read git describe directly Now that docs.yml checks out with fetch-depth 0, the tags are always present in CI, so the LATEST_TAG env + the `latest-tag` npm helper (with its shallow-clone fetch fallback) are redundant. Drop them and let the transform call `git describe` directly; restore the plain `docs`/`docs-dev` scripts. Co-Authored-By: Claude Opus 4.8 --- docs/_ext/inject-latest-tag.mjs | 16 ++++++---------- package.json | 5 ++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/docs/_ext/inject-latest-tag.mjs b/docs/_ext/inject-latest-tag.mjs index 343bee8..44ef4fa 100644 --- a/docs/_ext/inject-latest-tag.mjs +++ b/docs/_ext/inject-latest-tag.mjs @@ -10,22 +10,18 @@ * the browser), this file only ever runs under MyST (Node) at build time, so it * is free to touch git. It is loaded from `docs/myst.yml`, not shipped. * - * The tag is taken from `LATEST_TAG` (set by the `docs`/`docs-dev` npm scripts, - * which also handle fetching tags in a shallow CI checkout); if that is unset we - * fall back to `git describe` directly (the bare `myst build` case). When no tag - * can be resolved the sentinel is left in place and a warning is emitted, so a - * missing tag is loud rather than silently shipping `__LATEST_TAG__` to readers. + * The tag is read straight from `git describe` (CI checks out with fetch-depth 0 + * in docs.yml, so the tags are present). When no tag can be resolved the sentinel + * is left in place and a warning is emitted, so a missing tag is loud rather than + * silently shipping `__LATEST_TAG__` to readers. */ import { execSync } from "node:child_process"; const SENTINEL = "__LATEST_TAG__"; -/** The latest release tag: `LATEST_TAG` env, else `git describe`, else null. */ +/** The latest release tag (`git describe --tags --abbrev=0`), or null. */ function resolveLatestTag() { - const fromEnv = process.env.LATEST_TAG?.trim(); - if (fromEnv) return fromEnv; try { - // No fetch here — the npm scripts own that. This just reads what's local. return execSync("git describe --tags --abbrev=0", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], @@ -54,7 +50,7 @@ const injectLatestTag = { const tag = resolveLatestTag(); if (!tag) { vfile?.message?.( - `inject-latest-tag: no tag resolved (set LATEST_TAG or fetch git tags); leaving ${SENTINEL} as-is.`, + `inject-latest-tag: no tag resolved from git describe; leaving ${SENTINEL} as-is.`, ); return; } diff --git a/package.json b/package.json index a3ddc52..9835b32 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,8 @@ }, "scripts": { "test": "node test/test-url-logic.mjs && node test/test-assemble.mjs", - "latest-tag": "git describe --tags --abbrev=0 2>/dev/null || (git fetch --tags --quiet 2>/dev/null && git describe --tags --abbrev=0)", - "docs": "LATEST_TAG=\"$(npm run -s latest-tag || true)\" sh -c 'cd docs && myst build --html'", - "docs-dev": "LATEST_TAG=\"$(npm run -s latest-tag || true)\" sh -c 'cd docs && myst start'" + "docs": "cd docs && myst build --html", + "docs-dev": "cd docs && myst start" }, "files": [ "plugins/version-switcher.mjs",