Software release process
Three independent identities
The word “release” can refer to different things. These identities are related through provenance, but they are not interchangeable.
| Identity | Example | Meaning |
|---|---|---|
| Software version | toolkit-seascape 0.1.0 |
Code, Python API, CLI, schemas, algorithms, and packaging. |
| Scientific method version | geomorphometry_curvature_tpi_v2 |
The exact scientific interpretation implemented by a producer or transformation. |
| Data release | An immutable release ID or dated generation | Exact inputs, configuration, checksums, method versions, and materialized outputs. |
A rebuild with changed source inputs can create a new data release without changing the package, Git tag, GitHub Release, or PyPI version. Conversely, a package-only fix need not change a method identity or data-release identity. A change in scientific meaning must receive an explicit new method identity—even if it ships alongside an independently chosen software version.
Software releases
Software releases use Semantic Versioning and immutable tags named vX.Y.Z. The committed static
version in pyproject.toml, installed distribution version X.Y.Z, tag vX.Y.Z, and GitHub
Release version must agree exactly. The release workflow validates that invariant; it never
edits the version.
During the 0.x public-preview phase:
- Patch (
0.1.0→0.1.1) is for corrections and implementation, documentation, packaging, or performance fixes that preserve intended scientific meaning and public contracts. - Minor (
0.1.0→0.2.0) may contain documented API, schema, CLI, variable, producer, or method changes. Every scientific-meaning change must be prominent in the changelog and notes. - Major
1.0.0is reserved for a future stable public contract.
CHANGELOG.md is the reviewed record of user-facing changes. The workflow's GitHub Release notes
identify the public preview, installation command, and linked scientific changes and limitations.
PyPI publication
The software release path is:
merge to main → create version tag → immutable tagged checkout → version validation
→ build wheel and sdist once → Twine/distribution validation → clean consumer installation
→ upload validated files as a GitHub Actions artifact → GitHub Release
→ GitHub pypi environment approval → OIDC / Trusted Publishing → PyPI
The GitHub Release and PyPI upload consume the exact validated wheel and source distribution.
The PyPI job downloads the Actions artifact; it does not rebuild. Pull requests and pushes to
main do not run this publication path. The release job needs contents: write to create the
GitHub Release. Only the separate publishing job receives id-token: write.
No PyPI token or long-lived publishing credential is stored in GitHub. GitHub obtains a
short-lived OIDC identity for the publish-pypi job in the pypi environment; PyPI checks the
repository, release.yml workflow, and environment identity against its Trusted Publisher
configuration. Configure the GitHub environment with required reviewers and deployment
restrictions before publication.
In GitHub repository settings, create the pypi environment, require a reviewer, and allow
deployments from the main dispatch ref and release tags matching v*. As of 2026-09-30,
the PyPI project endpoint returned 404, so use PyPI's account-level pending publisher form
for the first upload. Register the distribution name toolkit-seascape, owner MarineCast,
repository toolkit-seascape, workflow filename release.yml, and environment pypi.
Do not create a GitHub secret. A pending publisher does not reserve the PyPI name until
the first successful upload.
PyPI package files and version numbers are immutable. A failed publication after the GitHub Release exists must use the documented backfill for the same validated assets, if possible, or a new software version. Never replace a PyPI version or move a Git tag.
Existing release backfill
The v0.1.0 tag and GitHub Release already exist, while PyPI returned no 0.1.0 version on
2026-09-30. Do not push v0.1.0 again. Once this workflow is on main and the GitHub pypi
environment and PyPI Trusted Publisher are configured, dispatch release.yml from main with
tag=v0.1.0. The backfill accepts only an existing version tag, checks out its exact commit,
confirms tag/version identity and the matching GitHub Release, and checks that the version is
absent from PyPI. It downloads the existing Release files, verifies their GitHub-recorded
SHA-256 digests, builds once from the tagged commit, and requires identical archive member names,
contents, types, and modes. It then runs Twine, distribution, and clean consumer validation on the
existing files before the PyPI job uploads those exact GitHub Release bytes. The backfill never
edits the tag or GitHub Release.
Rebuilding an old tag can change archive timestamps or owner fields even when member contents
are identical. If the content comparison fails, the backfill stops safely; investigate, then
release a new version from reviewed source rather than replacing v0.1.0 assets. Project URLs added
after the v0.1.0 tag cannot appear in its immutable wheel or sdist; they will appear in the next
version built from the updated source.
The tagged v0.1.0 metadata also retains an unbounded Rasterio dependency. As of 2026-09-30,
an ordinary Python 3.14 installation on macOS 14 can select Rasterio 1.5.2, which lacks a
compatible macOS 14 wheel and needs a separate GDAL source-build setup. The new source metadata
caps Rasterio below 1.5.2, but the backfill cannot change v0.1.0. Review this limitation before
choosing the first PyPI publication version.
For the first PyPI publication, use the normal v0.1.1 tag-triggered workflow after its
release checks pass. Do not dispatch the v0.1.0 backfill for this release.
Scientific method versions
Each scientifically meaningful producer or transformation records an explicit identity, such as:
geomorphometry_curvature_tpi_v2
survey_opportunity_asof_v2
bathymetry_slope_v1
The identifier describes interpretation—not a refactor, commit, or package version. A formula, threshold, support neighborhood, missingness rule, classification meaning, or other interpretive change requires review and a new method identity. Behavior-preserving implementation fixes need a software patch but should not fabricate a new scientific method.
Data releases
Materialized datasets have separate immutable identities. Their manifest should record the release ID and timestamp/date, software version, all method versions, source datasets and versions, source checksums, configuration, region/AOI, horizontal CRS and vertical datum, units and sign conventions, nodata semantics, and output checksums.
Data publication is intentionally outside the software release workflow. A data rebuild does not trigger PyPI publishing, a Git tag, or a GitHub software release. The tag workflow neither downloads scientific sources nor builds, uploads, or assigns identities to regional products.
Immutability
Never overwrite a Git tag, replace an existing GitHub Release artifact, republish the same PyPI version, or silently replace a scientific data release. Correct a problem by issuing a new software, method, or data identity as appropriate, and preserve the superseded record.
Maintainer procedure
- Complete the release checklist on the exact commit intended for release.
- Update the static project version and move reviewed changelog entries out of
Unreleased. - Merge to
mainand confirm required CI for that exact commit. - Create and push an annotated
vX.Y.Ztag. Do not move or reuse an existing tag. - The tag workflow checks the clean checkout and version, builds wheel and sdist once, runs Twine and installed-consumer validation, uploads the validated files, and creates the GitHub Release.
- Approve the
pypienvironment deployment. The dependent job publishes the uploaded files with PyPI Trusted Publishing after confirming the version is not already on PyPI. - Review the PyPI files, GitHub Release assets, and notes. Do not attach regional or generated scientific data.
The workflow uses gh release create for new tags; it fails if that release already exists.
For the existing v0.1.0 release, use gh workflow run release.yml --ref main -f tag=v0.1.0
only after the GitHub and PyPI configuration above is complete. This dispatch does not create
a new Git tag or GitHub Release.