Release process =============== How a new version of ``femorph-solver`` ships: bumping the version, the changelog, the docs-multiversion story for old-tag galleries. .. contents:: Page contents :local: :depth: 2 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``) 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.** .. code-block:: bash git tag -a v.. -m "Release .." git push origin v.. 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 — 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//``. * 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 :doc:`performance` 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 ----------------- .. list-table:: :header-rows: 1 :widths: 32 68 * - 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