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.

Open a pull request

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-thinning

The 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:

CheckWhat it measuresReproduce locally
Docs styleThe 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 TOCuv run python .claude/skills/docs-page/check_docs.py
LintBoth halves of ruff: that every Python file is formatted, and that it is free of lint errorsuv run lint
buildExecutes 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 rendercd docs && uv run myst build --html --execute --strict
bare installInstalls only [project].dependencies into a clean venv — the set a real user receives — then imports xmris and runs the processing chainuv 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 rangeuv 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/main

Then 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:

deploy.yml
      - 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 --strict

The 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

SymptomCauseFix
Docs style red, header '…' has no explicit (target)= above itMyST auto-slugs are numbered by document position, so they break on insertionAdd (page-topic-section)= above the header; link it as [text](#page-topic-section)
build red, Could not find DOI "…" from doi.orgDOI metadata is resolved over the network unless it is frozen in docs/myst.doi.bibcd docs && uv run myst build --doi-bib, then commit myst.doi.bib
build red, Site has N error(s), stopping buildA cross-reference, directive or link mystmd could not resolveReproduce with the build command above; the error names the file and line
build red only in CI, green locallyStale docs/_build cache locallyrm -rf docs/_build and rebuild
Preview link 404sThe Publish docs site workflow has not finished deploying yet, or (on a fork) the build is still waiting for a maintainer to approve itWait for the Docs preview status to appear; check the Actions tab for a run needing approval
Merge button blocked with everything greenThe branch is out of date in a way GitHub cannot merge, or a check never reportedCheck the merge box for which requirement is unmet; rebase only if there is a real conflict