Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Reference: the reusable workflows + contracts

The public interface is two reusable workflows consumed by uses: at a <tag>docs.yml directly from ci.yml, and publish.yml via a local publish-dispatch.yml shim (one per repo — see the tutorial):

jobs:
  docs:
    uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/docs.yml@<tag>
    with:
      build-command: make docs
  publish:
    needs: [docs]
    if: github.repository == 'ORG/REPO'
    uses: ./.github/workflows/publish-dispatch.yml
    with:
      version-name: ${{ needs.docs.outputs.version-name }}
    permissions:
      pages: write
      id-token: write
      contents: read
      actions: write
      statuses: write

The site-reconstruction logic (assemble/) is internalpublish.yml runs it directly; it is not a separately consumed action.

docs.yml — build (unprivileged)

Builds the docs at the versioned BASE_URL, packs docs.zip (bare html/ root), and uploads the docs artifact. Declares contents: read only — it never holds a write token. Installs uv unconditionally and relies on the runner’s preinstalled Node, so build-command can be make / npx / tox / npm driven.

inputrequireddefaultmeaning
build-commandyesCommand that builds the HTML site into html-dir at $BASE_URL. Fold any project setup (cp CONFIG, npm ci, apt deps) behind it.
html-dirnodocs/_build/htmlDirectory the build writes the site to; staged into docs.zip’s html/.
outputmeaning
version-nameThe version this build was served at (pr-<n> | default-branch | <tag>) — pass to publish.yml.

build-command runs with two env vars set: BASE_URL (/REPO/<version-name> — the sub-path the build is served at) and VERSION_NAME (the bare version token — for builds that need it directly, e.g. a Sphinx conf.py setting the pydata theme’s switcher version_match; see how-to: use with Sphinx).

publish.yml — the engine, owns the branching (privileged)

Reconstructs the whole site and deploys it to Pages, routing each event to one of three jobs. The deploy job carries the github-pages environment, concurrency: pages, and pages/id-token/statuses write. Reached only via the publish-dispatch.yml shim (below); the canonical-repo guard lives upstream (in ci.yml), so the engine stays generic.

Tags must deploy via workflow_dispatch: GitHub Pages silently drops a second deploy of an already-deployed SHA (a release tag shares the merge commit’s SHA) unless the event is workflow_dispatch (actions/deploy-pages#383; see the architecture explanation).

publish-dispatch.yml — the thin shim (one per repo)

The only thing that calls publish.yml, the single place you pin publish.yml@<tag>, and the dispatchable file the re-dispatch job re-fires. It just forwards to the engine via workflow_call (ci.yml’s publish, every event) and workflow_dispatch (the tag re-dispatch, a fork-PR preview, a manual re-deploy). A reusable workflow can’t be workflow_dispatch’d cross-repo, which is why this file lives in each repo.

publish.yml inputs (threaded through by the shim)

inputrequireddefaultmeaning
version-nameno""Version name of this run’s docs artifact to stage directly, instead of gathering it from durable sources. Set by ci.yml’s inline publish (the run isn’t a completed success yet, so the gather can’t discover it — or would find a stale previous build). Empty → pure durable gather (the dispatch paths).
prno""Fork PR number to approve (pins its head SHA as preview-approved) and preview. Set on the shim’s workflow_dispatch path.
dispatch-workflownopublish-dispatch.ymlFilename of the shim in the consumer’s repo, re-fired by the re-dispatch job. Override only if you renamed the shim.
retry-untilno""Internal — epoch-seconds deadline for auto-retrying a wedged Pages origin. Set only by the re-dispatch job (from the release’s publish time) when it re-fires the shim; carried unchanged through any retries. The shim must declare + forward this input (workflow_dispatch + the with: block below) even though consumers never set it themselves — re-dispatch always passes it, and a shim missing the input rejects the dispatch. Don’t set manually.

What publish.yml gathers

Every deploy rebuilds the complete tree from authoritative inputs:

version kindsourcedurability
current buildthis run’s docs artifact, staged via version-name (highest priority — overwrites any same-name gathered source)n/a (just built)
default branch (e.g. main)newest docs artifact built from that branch → durable _sources/<branch>.zip in the live site (hard-fail if neither exists)durable — re-persisted into the site each deploy
released tagsthe docs.zip asset attached to each GitHub Release (the migration seed release, if present, seeds the default-branch zip)permanent
open PRs (pr-<n>)each PR’s docs artifact, keyed by current head SHA — internal always, fork PRs only when the SHA carries a preview-approved statusephemeral — drops when the PR merges/closes

Branch and PR artifacts are found via the artifacts API by name (docs — the artifact name is the contract), not by workflow filename, so the consumer’s entry workflow can be called anything. The URLs baked into switcher.json (and the _sources restore) use the site’s live Pages URL from the Pages API, so a custom domain (CNAME) works.

A version no longer gathered (a merged/closed PR, a deleted release) is correctly dropped, because deploy-pages replaces the entire site. Sources are gathered in priority order (releases lowest, then branch CI, then the current build highest), so a fresher source always wins when names collide.

The docs.zip / version-name contracts

Two contracts let the build (docs.yml) and the reconstruction (assemble) agree without coordination — docs.yml owns both:

Internals: assemble.mjs

The pure logic (ordering, prerelease detection, switcher.json/redirect rendering, the required-branch guard) lives in assemble/assemble.mjs and is unit-tested (npm test) without git, the network, or the filesystem. The gh/unzip/mv IO plumbing lives as inline bash steps in publish.yml’s deploy job — named and separated so step timing and failures are visible in the GH Actions UI.