Skip to content
Draft
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
114 changes: 114 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Build the unified docs site (reusable).
#
# Called by:
# - preview.yml, for pull requests to this repo (once per OS; the macOS build is
# published as a browsable preview).
# - each member repository's docs.yml, for their pull requests: the whole site is
# built with that member at the PR's commit, and only warnings from that
# member's pages fail the build.
#
# The built site and the widget screenshots are uploaded as artifacts, and the
# job summary reports how long setup and the build took.
name: build

on:
workflow_dispatch:
workflow_call:
inputs:
os:
description: "Runner to build on"
type: string
default: macos-latest
member:
description: "Member (name in compose.toml) to build at `repository@ref`"
type: string
default: ""
repository:
description: "Repository (owner/name) to check that member out from"
type: string
default: ""
ref:
description: "Commit or branch of that member"
type: string
default: ""

jobs:
build:
runs-on: ${{ inputs.os || 'macos-latest' }}
defaults:
run:
shell: bash
env:
PYTHONUTF8: "1"
steps:
- run: echo "T0=$SECONDS" >> "$GITHUB_ENV"

# icons that some widgets fetch from the Iconify API (via pyconify), which
# rate-limits CI runners
- uses: actions/cache@v4
with:
path: ${{ runner.temp }}/pyconify-cache
key: pyconify-${{ runner.os }}-${{ github.run_id }}
restore-keys: pyconify-${{ runner.os }}-

- uses: actions/checkout@v6
with:
# when called from a member repo, check out the hub (not the caller)
repository: ${{ inputs.member && 'pymmcore-plus/pymmcore-plus.github.io' || github.repository }}
ref: ${{ inputs.member && 'main' || '' }}

- uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"

- name: Check out members
run: >
uv run --no-project tools/repos.py
${{ inputs.member && format('--pr {0}={1}@{2}', inputs.member, inputs.repository, inputs.ref) || '' }}

- run: uv sync
- uses: pymmcore-plus/setup-mm-test-adapters@main
# software OpenGL (Mesa) on Windows, for widgets with a vispy canvas
# (no-op on macOS; Xvfb and Qt libraries on Linux)
- uses: pyvista/setup-headless-display-action@v5
with:
qt: true
pyvista: false

- name: Build
env:
# use the OS's native Qt platform, so widget screenshots get its style
QT_QPA_PLATFORM: ${{ runner.os == 'Windows' && 'windows' || runner.os == 'macOS' && 'cocoa' || 'offscreen' }}
SCOPE: ${{ inputs.member || 'hub' }}
PYCONIFY_CACHE: ${{ runner.temp }}/pyconify-cache
run: |
echo "T1=$SECONDS" >> "$GITHUB_ENV"
uv run tools/compose.py build --strict --scope "$SCOPE"
echo "T2=$SECONDS" >> "$GITHUB_ENV"

- name: Report timing
if: always()
run: |
setup=$(( ${T1:-0} - ${T0:-0} ))
build=$(( ${T2:-$SECONDS} - ${T1:-0} ))
{
echo "### Docs build on ${{ runner.os }} (${{ inputs.os || 'macos-latest' }})"
echo "| step | seconds |"
echo "| --- | --- |"
echo "| setup (checkout, uv sync, adapters) | $setup |"
echo "| generate + build | $build |"
echo "| total | $(( setup + build )) |"
} >> "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@v4
with:
name: site-${{ runner.os }}
path: site
retention-days: 7

- uses: actions/upload-artifact@v4
with:
name: widget-screenshots-${{ runner.os }}
path: .repos/pymmcore-widgets/docs/images/generated
retention-days: 7
if-no-files-found: ignore
59 changes: 59 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Build and deploy https://pymmcore-plus.github.io from the main branch of every member.
#
# Triggered by pushes here, by members after they merge to main
# (`gh workflow run deploy.yml -R pymmcore-plus/pymmcore-plus.github.io`), and nightly.
name: deploy

on:
push:
branches: [main]
workflow_dispatch:
inputs:
source:
description: "What triggered this deploy (e.g. owner/repo@sha)"
required: false
schedule:
- cron: "17 5 * * *"

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"
- run: uv run --no-project tools/repos.py
- run: uv sync
- uses: pymmcore-plus/setup-mm-test-adapters@main

- name: Build
shell: bash # -o pipefail, so a failed build isn't hidden by `tee`
# not --strict: a warning in one member must not block deploying the others
run: |
echo "Triggered by: ${{ inputs.source || github.event_name }}"
uv run tools/compose.py build 2>&1 | tee build.log
{ echo '### Build warnings'; echo '```'; grep -iE 'warning|PROBLEM' build.log || echo none; echo '```'; } >> "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-pages-artifact@v4
with:
path: site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v4
id: deployment
54 changes: 54 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Build this repo's pull requests, and publish each as a browsable preview at
# https://pymmcore-plus.github.io/pr-preview/pr-<number>/ (via the gh-pages
# branch). A comment on the PR links to it; it is removed when the PR closes.
#
# The site is built on both macOS and Windows (to compare build times and
# widget screenshots); the macOS build is the one published.
name: preview

on:
pull_request:
types: [opened, reopened, synchronize, closed]

concurrency:
group: preview-${{ github.event.number }}
cancel-in-progress: true

permissions:
contents: write
pull-requests: write

jobs:
build:
if: github.event.action != 'closed'
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
uses: ./.github/workflows/build.yml
with:
os: ${{ matrix.os }}

deploy:
needs: build
if: ${{ !cancelled() && github.event.action != 'closed' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: site-macOS
path: site
- uses: rossjrw/pr-preview-action@v1
with:
source-dir: site
qr-code: false

remove:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: rossjrw/pr-preview-action@v1
with:
action: remove
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.repos/
build/
site/
.cache/
.venv/
__pycache__/
zensical.toml
compose.local.toml
uv.lock
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.2
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# pymmcore-plus.github.io

Source for **<https://pymmcore-plus.github.io>**: the unified documentation for
the pymmcore-plus ecosystem.

The site combines docs from several repositories into one site (one navigation,
one search, cross-references everywhere), built with
[zensical](https://zensical.org):

| Member | Mounted at |
| --- | --- |
| [pymmcore-plus](https://github.com/pymmcore-plus/pymmcore-plus) | `/pymmcore-plus/` |
| [useq-schema](https://github.com/pymmcore-plus/useq-schema) | `/useq-schema/` |
| [pymmcore-widgets](https://github.com/pymmcore-plus/pymmcore-widgets) | `/pymmcore-widgets/` |
| [pymmcore-gui](https://github.com/pymmcore-plus/pymmcore-gui) | `/pymmcore-gui/` |
| [napari-micromanager](https://github.com/pymmcore-plus/napari-micromanager) | `/napari-micromanager/` |

Each member keeps its docs in its own `docs/` folder (so PRs can change code and
docs together). This repository holds the theme, the landing page, *Getting
Started*, the site-wide navigation, and the tooling that composes everything.

## Quick start

Requires [uv](https://docs.astral.sh/uv/) and [just](https://just.systems).

```sh
just serve # clone members into .repos/, assemble, serve on localhost:8000
just build # build into site/
just check # strict build: fails on any warning
```

`uv run tools/compose.py serve --only pymmcore-plus,useq-schema --no-gen` gives
a faster preview of just some members, skipping their generation scripts.

To work on a member's docs (or code) with live reload, make `.repos/<member>` a
git worktree of your own clone:

```sh
git -C ~/dev/pymmcore-plus worktree add "$PWD/.repos/pymmcore-plus" -b my-docs-branch
```

## How it works

```text
compose.toml members (name, GitHub repo, branch)
zensical.base.toml theme, markdown extensions, plugins, global mkdocstrings options
redirects.toml old URL -> new URL, for pages that moved
docs/ hub pages, docs/.nav.yml (site navigation), assets/
overrides/ theme template overrides (page actions, package badge)
tools/repos.py clone / update / worktree member repos into .repos/
tools/compose.py generate -> copy -> API pages -> zensical.toml -> build
tools/api.py de-duplicated API reference generation
tools/pymm_mdx.py Markdown extension (re-export anchors, small HTML tweaks)
tools/check_urls.py verify every URL of the old sites still resolves
```

`tools/compose.py`:

1. runs each member's optional `docs/_gen.py` (generated content, screenshots);
2. copies the hub's `docs/` and each member's `docs/` into `build/docs/`,
expanding `--8<--` snippets relative to each member's repository and adding
`edit_url` / `package` front matter;
3. generates API reference pages from each member's `api-autonav` settings,
documenting every object once, at its highest-level public name;
4. merges member `zensical.toml` contributions (mkdocstrings `paths`, griffe
`extensions`, inventories) into `zensical.base.toml` -> `zensical.toml`;
5. builds with zensical, then writes per-member `objects.inv` files.

See [How these docs work](https://pymmcore-plus.github.io/contributing/docs/)
for the member-facing contract. When zensical gains native support for
composing projects, steps 2-4 are what it should replace.

## Deployment

- `.github/workflows/deploy.yml` builds from every member's `main` and deploys
to GitHub Pages: on push here, when a member merges to `main` (members call
`gh workflow run deploy.yml`), and nightly.
- `.github/workflows/preview.yml` builds pull requests to this repo (on macOS
and Windows, reporting build times) and publishes the macOS build at
`https://pymmcore-plus.github.io/pr-preview/pr-<number>/`, via the `gh-pages`
branch.
- `.github/workflows/build.yml` is the reusable build, also called by member
PRs to build the whole site with the PR's changes (uploaded as an artifact).
- `compose.overrides.toml` (temporary) points CI at members' unmerged
`zensical-docs` branches; delete it once those are merged.

Members need an org-level GitHub App (`DOCS_APP_ID` variable +
`DOCS_APP_PRIVATE_KEY` secret) with `actions: write` on this repository to
trigger deploys.
9 changes: 9 additions & 0 deletions compose.overrides.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# TEMPORARY: until the member repos merge their docs changes, CI builds them from
# these branches (fresh clones only; local checkouts in .repos/ are not touched).
# Delete this file before merging.
[overrides]
pymmcore-plus = "tlambert03/pymmcore-plus@zensical-docs"
useq-schema = "tlambert03/useq-schema@zensical-docs"
pymmcore-widgets = "tlambert03/pymmcore-widgets@zensical-docs"
pymmcore-gui = "tlambert03/pymmcore-gui@zensical-docs"
napari-micromanager = "tlambert03/napari-micromanager@zensical-docs"
36 changes: 36 additions & 0 deletions compose.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Members of the unified docs site.
#
# Each member is checked out at .repos/<name> (see tools/repos.py) and its
# docs/ folder is mounted at /<name>/ in the built site. Members own their
# pages, their .nav.yml files, and a minimal zensical.toml (mkdocstrings paths,
# griffe extensions, api-autonav settings).

[[member]]
name = "pymmcore-plus"
repo = "pymmcore-plus/pymmcore-plus"
title = "pymmcore-plus"
pypi = "pymmcore-plus"

[[member]]
name = "useq-schema"
repo = "pymmcore-plus/useq-schema"
title = "useq-schema"
pypi = "useq-schema"

[[member]]
name = "pymmcore-widgets"
repo = "pymmcore-plus/pymmcore-widgets"
title = "pymmcore-widgets"
pypi = "pymmcore-widgets"

[[member]]
name = "pymmcore-gui"
repo = "pymmcore-plus/pymmcore-gui"
title = "pymmcore-gui"
pypi = "" # installed from GitHub for now; the PyPI release is an old pre-release

[[member]]
name = "napari-micromanager"
repo = "pymmcore-plus/napari-micromanager"
title = "napari-micromanager"
pypi = "napari-micromanager"
Loading
Loading