Your change is written and green locally. This page covers everything between that and it being on
main: how to name the commit, what the six checks that gate the merge actually measure, where to
read your change rendered as a website before anyone reviews it, and who merges. It applies to every
kind of change — a processing method, a widget, a docs page, a diary entry.
main is protected: it takes no direct pushes, so a pull request is the only route in.
1. Branch, and title it as a Conventional Commit¶
git checkout main && git pull
git checkout -b <type>/<short-slug> # e.g. feat/apodize-gauss, docs/domains-thinningThe pull request title becomes the commit subject on main, because pull requests are
squash-merged — so it is the title, not your individual commits, that has to read well in
git log. Use a Conventional Commit prefix: feat:,
fix:, docs:, refactor:, test:, ci:, chore:. A parenthesised scope is encouraged where
it narrows usefully — fix(packaging), chore(ci), feat(fitting) — and a ! before the colon
marks a breaking change.
The body is where you say what moved. If you consolidated documentation — thinned one page into another, gave a concept a new home — name the pages, because that is the part a reviewer cannot see from the diff alone. Write it for someone who was not in the discussion: at release time the changelog entry for your change is written from this body, not from the diff, and it carries your issue and pull-request numbers forward as its trail.
2. Open it against main¶
git push -u origin <your-branch>
gh pr create --base main --title "docs: thin the domains page into the contract" --body "…"Or push the branch and open it from the GitHub web interface — after a push, the repository page offers a Compare & pull request button, and the title and body fields are the same ones the command above fills in. Nothing downstream cares which route you took.
Every push to the branch re-runs the checks below and refreshes your preview.
3. Six checks gate the merge¶
All six must pass before the merge button works. Each one is reproducible locally with a single command — run it there first, since a local failure costs seconds and a CI failure costs minutes:
| Check | What it measures | Reproduce locally |
|---|---|---|
Docs style | The docs rules myst build stays silent about: a header with no explicit target, a dead .ipynb link, a drifted kernel label, a page missing from the TOC | uv run python .claude/skills/docs-page/check_docs.py |
Lint | Both halves of ruff: that every Python file is formatted, and that it is free of lint errors | uv run lint |
build | Executes every notebook and explainer in the documentation, then fails on any mystmd error — a broken cross-reference, an unresolvable DOI, a directive that did not render | cd docs && uv run myst build --html --execute --strict |
bare install | Installs only [project].dependencies into a clean venv — the set a real user receives — then imports xmris and runs the processing chain | uv venv /tmp/bare && uv pip install --python /tmp/bare/bin/python . && /tmp/bare/bin/python .github/scripts/bare_install_smoke.py |
test (3.10) and test (3.13) | The full suite — including the tutorials, which are the maths tests — on both ends of the supported Python range | uv run test |
Three things worth knowing about that table. The build check is why a notebook that hangs can never
reach main: it runs under a 20-minute ceiling, so a stuck kernel fails visibly instead of burning
six hours. And --strict is load-bearing — without it mystmd reports its errors and still exits 0,
which is how a dead link once survived on the site for months.
Lint is the newest, and it stops somewhere deliberate. ruff formats Python inside Markdown code
blocks as readily as inside .py files, which would put every snippet on this site in its hands —
and it flattens them: the leading-dot .xmr chains collapse onto one line, the aligned comment
gutters lose their gutter. Since every page here is executed as a test, correctness is gated
already; what is left is layout, and layout is an authoring decision. So pyproject.toml excludes
*.md from ruff entirely, and uv run ruff format . is safe to run over the whole tree.
bare install is the odd one out, and deliberately so: every other job installs with uv sync --all-extras --dev, which means the dependency set an actual pip install xmris produces is
exercised nowhere else. That gap once shipped a release whose import xmris raised
ModuleNotFoundError while all four other checks stayed green, so this job installs the way a user
does and refuses to trust anything the dev environment happens to provide.
Neither of the two remaining pieces gates anything, and neither is a job on this run. Publishing
the site — where your preview comes from — is a separate workflow that starts when build
succeeds, and reports back as a Docs preview status carrying the link. External links are checked
weekly on a schedule rather than on your branch, because a third-party URL rotting is not your
fault and should not redden your pull request.
4. Read your change on its own website¶
Within a few minutes a bot comments a link like
https://andrewendlinger.github.io/xmris/pr-preview/pr-123/That is your branch, built and fully executed — the same build the live site gets, not a lighter one. Plots are real plots, hidden asserts really ran. For anything reader-facing this is the review surface: prose, admonitions, mermaid diagrams and cell output only reveal what they actually look like once rendered. A dev-diary entry is reviewed here like any other page.
Two caveats. The link arrives a couple of minutes after the green build check, because
publishing is a second workflow that starts only once this one has finished — the comment is posted
when the site is actually live, so the link never points at nothing. And if you are contributing
from a fork, your first build waits for a maintainer to press Approve: GitHub does not run
workflows from a new contributor unprompted, and this repository executes every notebook in your
branch. Once approved you get the same preview, the same comment and the same status as anyone
else — the dashed arrow above is the only place your branch and the published site meet, and only
an artifact crosses it.
5. Drive it green, then hand off¶
Push fixes until all six checks pass. You do not need to keep the branch up to date with main
— the checks are not configured strictly, so an unrelated merge landing meanwhile will not force you
to rebase. Rebase only for a real conflict:
git fetch origin && git rebase origin/mainThen hand off: a maintainer reviews and merges. No approving review is required by the tooling, so a maintainer can merge their own work once it is green — the checks are the gate, not a rubber stamp. Cutting a release is a separate, maintainer-run workflow (Publishing).
How the documentation reaches the web¶
Deployment is continuous and has nothing to do with releases, and the built site is never stored
anywhere: each deploy reassembles it from scratch — main’s most recent build, plus one artifact
per currently open pull request — and publishes that whole tree to GitHub Pages. Your preview is
on the site because your pull request is open. Closing it triggers one more deploy, from which it
is simply absent; nothing removes it, and there is no cleanup step to fail. A weekly scheduled run
performs the same assembly, which keeps main’s build fresh.
(The diary entry tells why it works this way.)
That assembly is a second workflow, and the split is the load-bearing part. Documentation
builds your branch — executing every notebook in it — and holds nothing but a read-only token.
Publish docs site picks up the artifact afterwards and does everything privileged. GitHub runs
that second workflow from main’s copy of the file whatever branch triggered it, so the token that
can write to the site is never in reach of the branch being tested. Which is also why the assembly
picks artifacts by provenance — which repository and which commit produced them — rather than by
name: on a pull request the workflow file itself comes from your branch, so the name it writes on
an artifact is a hint, not a promise.
The one subtlety worth internalising if you ever touch the workflow: mystmd bakes the site’s base path into every asset link at build time, so a preview has to be built for the subdirectory it will be served from. That is what this step does, quoted from the workflow itself:
- name: Build MyST Site
env:
# BASE_URL is baked into every asset path at build time, so a preview
# has to be built for the subdirectory it will be served from --
# otherwise every stylesheet and script 404s.
BASE_URL: ${{ github.event_name == 'pull_request' && format('/xmris/pr-preview/pr-{0}', github.event.number) || '/xmris' }}
run: |
uv run docs-api
cd docs
uv run myst build --html --execute --strictThe build step — quoted from .github/workflows/deploy.yml at build time
Both workflows can be run by hand from the Actions tab (workflow_dispatch), and which one you
want depends on what broke. Publish docs site re-assembles and redeploys from the artifacts
that already exist — seconds, and the right lever when a deploy failed. Documentation rebuilds
first and then chains into publishing, which is the slower recovery you need when the artifact
itself is missing or stale — including the No usable 'site-main' artifact failure. Dispatch that
one from main: assembly only accepts a site-main built on main, so the same run launched
off a branch rebuilds happily and then fails identically. Either way a
Pages deployment is atomic, so the previous site keeps serving until a good one replaces it, and
the failure is loud rather than destructive.
When it goes wrong¶
| Symptom | Cause | Fix |
|---|---|---|
Docs style red, header '…' has no explicit (target)= above it | MyST auto-slugs are numbered by document position, so they break on insertion | Add (page-topic-section)= above the header; link it as [text](#page-topic-section) |
build red, Could not find DOI "…" from doi.org | DOI metadata is resolved over the network unless it is frozen in docs/myst.doi.bib | cd docs && uv run myst build --doi-bib, then commit myst.doi.bib |
build red, Site has N error(s), stopping build | A cross-reference, directive or link mystmd could not resolve | Reproduce with the build command above; the error names the file and line |
build red only in CI, green locally | Stale docs/_build cache locally | rm -rf docs/_build and rebuild |
| Preview link 404s | The Publish docs site workflow has not finished deploying yet, or (on a fork) the build is still waiting for a maintainer to approve it | Wait for the Docs preview status to appear; check the Actions tab for a run needing approval |
| Merge button blocked with everything green | The branch is out of date in a way GitHub cannot merge, or a check never reported | Check the merge box for which requirement is unmet; rebase only if there is a real conflict |