Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ git push origin --tags
(`--tags` pushes every local tag; on a clean clone that's just the new one. Use
`git push origin vX.Y.Z` to push only that tag.)

CI runs lint + tests + the docs build; `release.yml` creates the GitHub Release with
`version-switcher.mjs` + the tag's `docs.zip` attached (via `gh`); and the nested
publish (tag re-dispatch) reconstructs + deploys the site including the new tag. The plugin URL
and the `uses:` refs for the reusable workflows all resolve to the same tag, so one
tag versions both halves.
CI runs lint + tests + the docs build, and `release.yml` creates the GitHub Release with
`version-switcher.mjs` + the tag's `docs.zip` attached (via `gh`). When that CI run
completes, `publish.yml` picks it up on `workflow_run` and reconstructs + deploys the site
including the new tag — the release asset is already attached by then, because
`release.yml` ran inside the run publish is waiting on. The plugin URL and the `uses:`
refs for the reusable workflows all resolve to the same tag, so one tag versions both
halves.
6 changes: 2 additions & 4 deletions .github/workflows/_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ jobs:
run: npx prek run --all-files --show-diff-on-failure

# Workflow EXPRESSIONS are validated only by GitHub, and a bad one rejects the
# whole workflow before any job starts — no logs, no annotations, no failing step.
# That is how `|| "manual dispatch"` (expressions take single-quoted literals only)
# took the site's deploy down after it had passed YAML parsing, `bash -n`, and the
# workflow harness. actionlint catches exactly that class.
# whole workflow before any job starts — no logs, no annotations, no failing step,
# so nothing local catches it. actionlint does.
#
# Two errors are suppressed: actionlint's `job` context schema is stale and does not
# know `job.workflow_repository` / `job.workflow_sha`, both of which are real and
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ jobs:
- name: Unit tests (widget logic + make-switcher parity)
run: npm test

# publish.yml's gather steps are the most intricate code in the repo and used to
# be verifiable only by merging and watching a real deploy. The harness loads them
# out of the YAML and runs them against a mock gh, so they cannot drift.
# publish.yml's gather steps are the most intricate code in the repo, and a real
# deploy is the only other way to exercise them. The harness loads them out of the
# YAML and runs them against a mock gh, so the tests cannot drift from the workflow.
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0 # full version: setup-uv uses immutable releases
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ name: CI
# It does NOT publish the site. publish.yml picks this run up on `workflow_run` once it
# completes and deploys from there, so the site's reconstruction cost stays out of every
# PR's critical path and a Pages failure never shows as a red check against an author who
# cannot act on it. That also means this workflow needs no elevated permissions, and its
# `name:` below is load-bearing — publish.yml matches on it.
# cannot act on it. This workflow therefore needs no elevated permissions, and its `name:`
# below is load-bearing — publish.yml matches on it.
on:
pull_request:
push:
Expand Down
43 changes: 18 additions & 25 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,17 @@ on:
required: false
default: docs/_build/html
type: string
outputs:
version-name:
description: The version name this build was served at (pr-<n> | <default-branch> | <tag>).
value: ${{ jobs.build.outputs.version-name }}

# REUSABLE docs build — the UNPRIVILEGED half, shared across MyST projects. Builds
# the docs at the versioned BASE_URL and uploads this build's `docs` artifact
# (docs.zip, a single html/ root dir). It runs for every event — PRs (including forks),
# pushes to the default branch, and tags — but NEVER publishes. publish.yml (the
# privileged half) deploys, called separately by the consumer's ci.yml on internal
# events, injecting THIS build's artifact for the current version name.
# privileged half) picks the artifact up once the run completes.
#
# This workflow OWNS the build/publish contract so it cannot drift: the version
# name, the BASE_URL, the docs.zip single-root-dir shape, and the `docs` artifact name
# all live here. The consumer supplies only HOW to turn sources into HTML, via
# `build-command`.
# This workflow owns one half of the build/publish contract: the docs.zip
# single-root-dir shape and the `docs` artifact NAME (publish.yml gathers cross-run
# artifacts by that name via the artifacts API, never by workflow filename). The
# consumer supplies only HOW to turn sources into HTML, via `build-command`.
#
# Environment given to build-command: Node/npx (preinstalled on the runner) and uv
# (always installed; honours a committed .python-version). So build-command can be
Expand All @@ -42,8 +37,6 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
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
Expand All @@ -68,8 +61,14 @@ jobs:
enable-cache: false

# Version name = the site sub-dir this build is served at, and the BASE_URL it
# must be built with. pr-<n> for PRs; otherwise the ref name (default branch or
# a tag without `/`). No sanitisation: every version name is URL/path-safe.
# must be built with: pr-<n> for PRs, otherwise the ref name (the default branch,
# or a tag without `/`). No sanitisation — every version name is URL/path-safe.
#
# This is the OTHER half of the contract, and it is not passed anywhere: the
# gather independently re-derives the same name for each source it stages (a
# release's tag, a PR's number, the repo's default_branch) and unpacks the zip at
# site/<that name>. Both sides compute the same string from the same facts, so
# this rule and assemble.mjs's dest rule must change together.
- name: Compute version name
id: ver
run: |
Expand All @@ -82,9 +81,8 @@ jobs:
echo "version-name=$name" >> "$GITHUB_OUTPUT"

# BASE_URL must match the versioned sub-path the build is served at, or its
# root-absolute assets 404. assemble files this build's artifact at the same
# version name (site/<default-branch>, site/<tag>, site/pr-<n>), so the two
# cannot drift. VERSION_NAME is the bare version token, exported for builds
# root-absolute assets 404 — see the version-name contract above.
# VERSION_NAME is the bare version token, exported for builds
# that need it directly — e.g. a Sphinx conf.py setting pydata-sphinx-theme's
# switcher `version_match` — without parsing it back out of BASE_URL.
# build-command comes through an env var (not interpolated into the script)
Expand All @@ -100,10 +98,9 @@ jobs:

# Pack the site as docs.zip with a single root directory — the durable contract
# both publish.yml's gather and release.yml's asset rely on. Stage into html/
# first so any html-dir name yields the same root. publish.yml's extract keys on
# there being exactly ONE root dir, not on it being called html/ (release assets
# are durable and may have been packed by another pipeline), but everything this
# workflow produces is html/-rooted.
# first so any html-dir name yields the same root. The extract side keys on there
# being exactly ONE root dir, not on its name, but everything this workflow
# produces is html/-rooted.
- name: Pack docs.zip (single html/ root)
env:
HTML_DIR: ${{ inputs.html-dir }}
Expand All @@ -123,7 +120,3 @@ jobs:
path: ${{ runner.temp }}/docs.zip
compression-level: 0

# NOTE: nothing here tells a fork PR that its docs will not be published. There is
# no longer anywhere sensible to say it: publish.yml excludes forks before the
# engine is reached, so no job runs in that case at all. This build job stays
# purely about building + uploading the docs artifact.
Loading