Release process#

How a new version of femorph-solver ships: bumping the version, the changelog, the docs-multiversion story for old-tag galleries.

Versioning#

The package version lives at src/femorph_solver/_version.py (driven by setuptools-scm via the version_file knob in pyproject.toml). At build time, setuptools-scm writes the resolved version into _version.py based on the latest annotated git tag.

The semver mapping is the standard one:

  • Major — public API breaks; stale-tag galleries may stop working. Reserved for genuine compatibility breaks (currently 0.x; pre-1.0 doesn’t promise stability).

  • Minor — new features, new VM rows, new element kernels, new readers. Backward-compatible. This is the common cadence.

  • Patch — bug fixes, doc tweaks, perf improvements that don’t move public APIs.

setuptools-scm derives intermediate-release strings (e.g. 0.21.dev3+g<hash>) automatically when commits are ahead of the latest tag. Don’t hand-edit _version.py.

Cutting a release#

Steps, in order:

  1. Pre-flight. Confirm CI is green on main, the release-readiness workflow’s diff against the previous tagged release shows no perf regressions ≥ 10 %, the docs build is clean.

  2. Update CHANGELOG.rst (under doc/source/changelog.rst — populated since #619). Move the in-progress section to the new version’s heading; group entries by category (Features, Fixes, Docs, Verification, Internal). Cite the PR number for each entry.

  3. Tag.

    git tag -a v<MAJOR>.<MINOR>.<PATCH> -m "Release <MAJOR>.<MINOR>.<PATCH>"
    git push origin v<MAJOR>.<MINOR>.<PATCH>
    
  4. Wheel build + upload. The release CI workflow handles PyPI upload via Trusted Publisher; no API tokens to manage.

  5. Multiversion docs build. The docs workflow on the tag fires sphinx-multiversion with the new tag in scope; the per-version sidebar updates automatically (see below).

  6. Announcement. GitHub release notes are auto-populated from the changelog. Edit if needed.

Changelog conventions#

Phase 5 of #583 populated the changelog from v0.20.0 onwards. The conventions:

  • One section per version, headed v<X.Y.Z> YYYY-MM-DD.

  • Within a section, entries are grouped under Features, Fixes, Docs, Verification, Internal.

  • Each entry is a one-liner that names what shipped, with the PR number in parentheses. Example:

    Features
    --------
    
    * Add ``StressSpec`` to the verification registry —
      unblocks NAFEMS LE1 and Kirsch K_t = 3 stress assertions
      (#626).
    
  • Cross-cutting changes (workflow / kernel / interop) reference any tracker / project they relate to (#345, #511, #601).

  • Don’t paraphrase the PR title — the changelog is for searchable history, not marketing.

The “in-progress” section header is Unreleased; entries accumulate there until the cut.

sphinx-multiversion#

The docs deploy ships multiple versions in one site so users on older releases can find the docs that match their installed version. The implementation is sphinx-multiversion (config in doc/source/conf.py).

How it works:

  • When the docs CI runs sphinx-multiversion (see the docs workflow), it walks the git history and picks out every annotated tag matching the version pattern, plus main.

  • For each, it checks out the tag, runs Sphinx, and writes the built HTML under _build/html/<version>/.

  • A version switcher in the sidebar (powered by doc/source/_static/switcher.json) lets readers move between versions.

Caveat that bit us in the past#

sphinx-multiversion checks out each tag’s source tree but runs against the currently installed library. Old-tag galleries that import from the installed femorph_solver see the new library’s API, not the API the tag used to ship. This caused issue #487 — a 1-D displacement shape compatibility shim had to land in static_result_to_grid so old-tag examples kept rendering.

The implication for authors: when you change a public API, the old-tag galleries are part of the test surface. Either:

  • Keep a back-compat shim in the new code (the standard approach — code stays simple, old galleries work).

  • Or accept that the docs deploy fails until the switcher.json cuts the old tag from the version list.

Don’t surprise this on a release.

The release-readiness gate#

Before any tag fires, the release-readiness workflow has to be green on the commit that’s about to be tagged. See Performance — profiling, snapshots, the perf tracker for what that workflow tracks; the short version is “perf regressions ≥ 10 % block the docs deploy.”

If a release is urgent and a regression is acknowledged-but- unfixed, the workflow can be workflow_dispatch-bypassed by a maintainer with a written explanation in the release notes. Don’t bypass casually — the perf history breaks if the bypass isn’t documented.

Yanking a release#

Mistakes happen. When a release ships with a broken bit:

  1. yank on PyPI marks the version unbroken-installable but not eligible for pip install femorph-solver without a pin.

  2. Push a patch release ASAP that fixes the bug; the CHANGELOG entry for the patch cites the yank.

  3. Don’t delete the git tag. The yanked version is part of the history; deleting the tag breaks sphinx-multiversion for any reader on that version.

Common pitfalls#

  • Hand-editing _version.py. setuptools-scm rewrites it on every build; the edit is silently overwritten.

  • Tagging without a clean main. The release CI ties the wheel to the commit at the tag; an unclean tree means an un-reproducible release.

  • Forgetting to update the switcher.json. Adding a new version requires a switcher entry so the version dropdown shows it. The CI doesn’t auto-populate it (yet — that’s a Phase 5 follow-up).

  • Backward-incompatible API change with no shim. Old-tag galleries break. Either land a shim or coordinate the cut in switcher.json in the same release.

  • Empty changelog section. A release with no changelog entries means either nothing of note shipped (in which case why release?) or the entries were missed during the cut. The reviewer flags this in the release PR.

Where things live#

Concern

Path

Version source-of-truth

src/femorph_solver/_version.py (auto-generated by setuptools-scm)

setuptools-scm config

pyproject.toml (the [tool.setuptools_scm] block)

Changelog

doc/source/changelog.rst

Multiversion config

doc/source/conf.py (smv_* knobs)

Version switcher

doc/source/_static/switcher.json

Docs build script

doc/Makefile and the docs workflow

Release-readiness gate

.github/workflows/release-readiness.yml

GitHub release notes

auto-populated from the tagged CHANGELOG.rst entry