Publishing¶
Two artifacts are published from this repository: the documentation site on GitHub Pages, and the package on PyPI.
Documentation¶
The site is built with MkDocs Material and deployed to GitHub Pages by GitHub Actions.
Locally¶
make install # uv sync --group dev --group docs
make docs # mkdocs serve, with live reload
make docs-build # mkdocs build --strict
--strict turns warnings into errors, which is what catches a broken internal
link, a missing snippet marker, and a page that exists but is absent from the
navigation. Run it before pushing; CI runs exactly the same command.
The deploy workflow¶
.github/workflows/deploy-docs.yml runs on pushes to main that touch the
documentation, and can be triggered by hand with Run workflow:
on:
push:
branches:
- main
paths:
- 'docs/**'
- 'examples/**'
- 'mkdocs.yml'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/deploy-docs.yml'
workflow_dispatch:
examples/** is in that list because the guides embed their code from there —
an example change must redeploy the site.
Two jobs. The first builds and uploads the artifact:
- name: Build documentation
run: uv run --group docs mkdocs build --strict
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site
The second deploys it:
path: site matches site_dir: site in mkdocs.yml. The workflow uses a
github-pages concurrency group with cancel-in-progress: false, so
overlapping deploys queue rather than abort halfway.
One-time repository setup¶
The artifact-based deployment needs Pages configured for Actions rather than for a branch:
- Open the repository Settings.
- Go to Pages.
- Under Build and deployment, set Source to GitHub Actions.
Without this the workflow fails at the deploy step with a permissions error. No
gh-pages branch is created or used.
The workflow already requests the permissions it needs:
Validation before deploy¶
.github/workflows/test.yml runs on every push and pull request, including a
docs job that builds the site strictly. The doctests embedded in the pages run
in the test job alongside the unit tests, on every supported Python version.
So a documentation change is checked twice before it can reach main: the
transcripts must still produce their stated output, and the site must still
build. See How These Docs Are Tested.
Package¶
The package is published to
pypi.org/project/open-fsm and built with
the uv_build backend. The version lives in exactly one place,
pyproject.toml; open_fsm.__version__ reads it back from the installed
distribution metadata.
One-time PyPI setup¶
Releases upload through Trusted Publishing, so no API token is stored in the repository. An owner configures the publisher once with these values:
| Field | Value |
|---|---|
| PyPI Project Name | open-fsm |
| Owner | open-byte |
| Repository | open-fsm |
| Workflow | publish.yml |
| Environment | pypi |
Where that form lives depends on whether the project already exists on PyPI:
- Before the first release, the project has no settings page yet, so it is registered as a pending publisher at Account settings → Publishing. PyPI creates the project on the first successful upload and converts the pending publisher into a normal one.
- Afterwards, at Manage project → Publishing.
The GitHub pypi environment must also exist, under Settings → Environments
in the repository. It can be empty; the workflow only needs the name to match.
Cutting a release¶
uv version --bump patch # or --bump minor / --bump major
uv lock # the lock records the project version too
Commit the result, merge it to main, then tag and publish a GitHub Release
whose tag is the version prefixed with v (v0.0.2 for 0.0.2):
Publishing the release triggers
publish.yml,
which refuses to continue when the tag and the packaged version disagree, then
builds, validates the metadata with twine check, and uploads.
A version number is spent the moment it uploads
PyPI does not allow re-uploading a version, even after deleting it. That is
why the workflow compares the tag against uv version --short before
building — a release tagged v0.0.2 that ships 0.0.1 would burn the wrong
number permanently. Rehearse on TestPyPI first.
Local verification¶
Build and inspect the artifacts without uploading anything:
Rehearsing on TestPyPI¶
TestPyPI is a separate site with its own account and its own tokens — a PyPI token will not work there. Create one at test.pypi.org → Account settings → API tokens.
Pass it through the environment so it never lands in shell history or a tracked file:
make publish-test builds and uploads to the testpypi index declared in
pyproject.toml. Then install from there into a throwaway environment:
uv venv /tmp/open-fsm-smoke
VIRTUAL_ENV=/tmp/open-fsm-smoke uv pip install \
--index-url https://test.pypi.org/simple/ \
open-fsm
VIRTUAL_ENV=/tmp/open-fsm-smoke uv run python -c \
"import open_fsm; print(open_fsm.__version__)"
No --extra-index-url is needed: open-fsm has no runtime dependencies, so
TestPyPI can satisfy the install on its own. A project with dependencies would
need PyPI as the primary index plus --index-strategy unsafe-best-match, since
uv otherwise stops at the first index it consults.
TestPyPI versions are spent too
The same no-reupload rule applies. If a rehearsal burns 0.0.1 there, bump
to a throwaway local version (uv version 0.0.1.dev1) for the next attempt
rather than fighting it, and reset before the real release.
Supported versions¶
Every Python version the package advertises is executed by the test job in
test.yml,
which runs the matrix from 3.10 through 3.15. When requires-python or a
classifier changes, add or move a cell in that matrix so the claim stays tested.