Release checklist
Run every item before tagging a new pybmodes version. The goal
is not “the build looks green” — it’s “nothing about the release
is unverified”. Each step is a quick local command plus an explicit
expected outcome.
0. Prerequisites
Activate the dev environment (Windows + conda example; adapt for your shell):
call C:\Users\<you>\miniconda3\Scripts\activate.bat pybmodes
set PYTHONPATH=%CD%\src
Working tree should be clean before starting:
git status
# expected: "nothing to commit, working tree clean"
1. Default test suite (self-contained, no external data)
pytest -q
Expected: every collected test passes. The default run skips integration-marked tests cleanly (see Validation matrix for the list of what needs external data).
2. Integration test suite (needs local OpenFAST + BModes decks)
First, fetch (if needed) and verify the local external/ tree
against the pinned manifest:
python scripts/verify_external_data.py --clone # fetch missing required clones
python scripts/verify_external_data.py --strict
Expected from --strict: 0 WARN, 0 FAIL with PASS on every
required clone. SKIP is fine for the optional = true
entries (MoorPy / RAFT / BModes) when they’re absent. A WARN means
a SHA pin is TBD — bump those before release. A FAIL means a
required clone is missing or has drifted from its pinned SHA / file
hash; --clone fetches the missing ones, or
git -C external/<clone> checkout <sha> fixes a drift.
Then run the integration suite:
pytest -m integration -q
Expected: every collected test passes. If you don’t have the
upstream decks cloned under external/ and pinned per
external/MANIFEST.toml, this step exits with code 5 (“no
tests collected”) — that’s acceptable for a local pre-tag pass
only if you’ve separately verified the integration track on
another machine that does have the data. CI runs both steps;
the integration job’s exit-5 path is allowed but every other
failure mode is a hard fail.
Note
The pinned manifest is the published reproducibility
contract for the validation matrix’s external-data tracks:
manifest-pinned commit SHAs plus line-ending-normalized
SHA-256 hashes for the text validation decks. The public
required set (r-test + IEA-3.4 / 10 / 15 / 22 + WISDEM) is
CI-gated by validation.yml and reproducible by anyone
with the manifest; only the BModes archive (a NREL download,
not publicly clonable) and the optional cross-comparison
clones (MoorPy / RAFT) stay maintainer-local. Treat the
manifest pins as part of the API contract — bumping a pin is
a deliberate maintainer action documented in the corresponding
CHANGELOG.md entry under Changed.
3. Linting + type checking
python -m ruff check src/ tests/ scripts/
python -m mypy src/pybmodes
Expected: both clean. scripts/ is gated because user-facing
workflows (build_reference_decks, campbell,
visualise_*) live there and any regression in them is
user-visible.
4. Sample-input verifier
python src/pybmodes/_examples/sample_inputs/verify.py
Expected: every analytical-reference sample passes at < 1 % RMS
against its closed-form reference. Output ends with a summary line
like Result: 4/4 sample case(s) passed..
4.5. Validation-matrix audit
python scripts/audit_validation_claims.py
Expected: OK: every VALIDATION.md test-file reference exists and
contains at least one test method. The script parses every
tests/... link in VALIDATION.md, asserts the path exists,
and asserts the file (or directory glob) contains at least one
def test_… method — catching “claim ahead of test” drift where
the matrix advertises behaviour with no enforcing test. A non-zero
exit is a release blocker; either add the missing test or remove
the row from the matrix before tagging.
5. Reference-deck regeneration + validator
python scripts/build_reference_decks.py
Expected: every case in the manifest builds successfully; the
post-patch validation report ends in Overall: PASS or
Overall: WARN. A FAIL verdict on any case is a release
blocker. The IEA-15 UMaine VolturnUS-S case is expected to end in
WARN on TwSSM2Sh — that’s documented in
src/pybmodes/_examples/reference_decks/FLOATING_CASES.md and
src/pybmodes/_examples/reference_decks/iea15mw_umainesemi/validation_report.txt’s
footer; treat any other WARN as new and investigate before
shipping.
6. Walkthrough notebook smoke-check
The walkthrough notebooks ship source-only — committed without
executed cell outputs (no stored figures). CI executes every cell
headlessly via tests/test_notebooks.py; reproduce that here:
pytest tests/test_notebooks.py # synthetic walkthrough (default)
pytest -m integration tests/test_notebooks.py # the two IEA-15 notebooks (needs external/ data)
Expected: notebooks/walkthrough.ipynb executes in the default run;
the two cases/ IEA-15 notebooks execute under the integration
marker once the upstream decks are present (and assert the friendly
FileNotFoundError contract when the data is absent).
To eyeball the rendered figures (optional — outputs aren’t committed), execute one to a transient copy and open it, then delete it:
jupyter nbconvert --to notebook --execute notebooks/walkthrough.ipynb --output _smoke.ipynb
# ...inspect notebooks/_smoke.ipynb, then remove it (transient artefact)
7. Case scripts (optional — produce PNGs under outputs/)
for case in cases/bir_2010_land_tower cases/bir_2010_monopile \
cases/bir_2010_floating cases/nrel5mw_land \
cases/iea3mw_land cases/nrel5mw_monopile; do
python "$case/run.py"
done
Expected: each writes its PNGs without raising. These are
local-data-dependent for the BModes case-test decks; the cases
under cases/nrel5mw_*/ need external/OpenFAST_files/r-test/
and the IEA-3.4 case needs
external/OpenFAST_files/IEA-3.4-130-RWT/. Missing-data exits
should be obvious from the per-case error message.
8. Version + CHANGELOG promotion
pyproject.toml: bumpversion = "X.Y.Z"from the previous tag’s value.src/pybmodes/__init__.py: bump the dev fallback string__version__ = "X.Y.Z-dev".CITATION.cff: bump the softwareversion:toX.Y.Zanddate-released:to the release date (this is theversion:field, not thecff-version:schema field).tests/test_version.pygates this againstpyprojectso a forgotten bump fails CI.CHANGELOG.md: promote the## [Unreleased]block to## [X.Y.Z] — YYYY-MM-DD; reset[Unreleased]to(nothing yet).
Commit with a stand-alone message like
chore: bump version to X.Y.Z, promote CHANGELOG. Verify the
commit’s stat shows only those four files changed.
9. Tag + push
git push origin master
git tag -a vX.Y.Z -m "pyBmodes X.Y.Z — <one-line release headline>"
git push origin vX.Y.Z
The v prefix is the standard convention PyPI, GitHub Releases,
and conda-forge all expect. Push the master branch before the
tag so the tag refers to a commit that’s on the remote.
Important
Run the Validation (external data) workflow on the release
commit and confirm it is green before pushing the tag. The
publish workflow’s validation-gate job refuses to publish
unless a successful validation.yml run exists for the exact
commit the tag points at. After git push origin master,
dispatch it from the Actions tab (Validation (external data) →
Run workflow → master), wait for green, then push the tag.
The tag push fires the PyPI publish workflow
(.github/workflows/publish.yml) — see step 10 below. Don’t
push the tag until you’re ready for PyPI to receive the artefact.
10. PyPI publish (automatic, but verify)
Pushing the tag fires Publish to PyPI via Trusted
Publishing. Watch the workflow on the Actions tab:
build-and-smoke— builds sdist + wheel, asserts the tag matchespyproject.tomlversion, smoke-installs both into fresh venvs. A failure here usually means a forgottenMANIFEST.inentry or a[project] versionline that doesn’t match the tag.validation-gate— asserts a green Validation (external data) run exists for this commit. If you skipped the dispatch in step 9 this fails closed; run the validation workflow on the commit, wait for green, and re-run this job (or re-tag).publish— pulls the built artefacts and uploads to PyPI via the OIDC handshake (no API token). Thepypienvironment may have required-reviewer protection turned on; approve the deployment on the Actions UI if so.
After the workflow goes green, verify on PyPI:
# in a throwaway venv
python -m venv /tmp/pypi-check
/tmp/pypi-check/bin/pip install --upgrade pip
/tmp/pypi-check/bin/pip install pybmodes==X.Y.Z
/tmp/pypi-check/bin/python -c "import pybmodes; print(pybmodes.__version__)"
Expected: prints X.Y.Z exactly. If pip can’t find the
version, give PyPI a few minutes to propagate to its CDN.
Note
Trusted Publishing must be configured PyPI-side first.
The maintainer registers pybmodes on PyPI, adds a
Trusted Publisher entry pointing at this repository +
publish.yml + the pypi environment, and creates a
matching environment under the repo settings. The
publish.yml header carries the exact configuration.
First-time setup takes ~5 min in the PyPI UI; subsequent
releases are token-less and automatic.
11. GitHub Release
On https://github.com/SMI-Lab-Inha/pyBModes/releases/new :
Choose tag:
vX.Y.Z.Release title:
pyBmodes X.Y.Z.Paste the relevant
## [X.Y.Z]section fromCHANGELOG.mdas the release notes body. Add a brief Highlights section above the detailed changelog if the changeset is large enough to warrant one (the X.Y.0 minor-bumps usually do; patch-only bumps usually don’t).Attach the verifier report as a release asset: from the release-checklist machine, run
python scripts/verify_external_data.py --strict > verify-vX.Y.Z.txtand upload that file. Anyone reproducing the integration tolerance against the BModes Fortran reference can re-run the same verifier to confirm the manifest pins.Set as the latest release: ✓ (unless this is a back-port).
Publish.
The GitHub Actions CI badge in README will repaint to green on the new tag’s commit automatically.
11. Post-release sanity
git fetch --tags
git tag -l "v*" | tail -5
Expected: the new tag is in the list and matches what’s on origin.
pip install -e . --quiet
python -c "import pybmodes; print(pybmodes.__version__)"
Expected: the version reported matches the tag exactly (no
-dev suffix — the install picks up the value from
pyproject.toml).
If any step fails, do not push the tag. Fix the underlying issue and re-run from the point of failure. The checklist exists because the cost of a botched public tag (deleting it, retagging, re-publishing) is much higher than the cost of running through ten local verifications first.