Releasing an official USGS software information product#

gdptools is a public open-source software project. That is not the same thing as an official USGS software information product — only the latter is citable by other USGS information products, and only the latter carries a DOI.

This page is the procedure for producing one. For an ordinary engineering release (version bump, tag, PyPI, conda-forge) see RELEASING.md instead — that process stops short of everything below.

Open a tracking issue from the USGS Official Release issue template before you start; it carries this procedure as a checklist you can tick through, and doubles as the review artifact for IPDS.

What policy requires#

Five things, per the USGS Git and Software Release course:

  1. Proper license, disclaimer, and metadata (code.json)

  2. Appropriate review(s) and approval as defined by current policy

  3. An approved Information Product Data System (IPDS) record

  4. A Git tag, and a GitLab Release corresponding to that tag

  5. A digital object identifier (DOI)

Local policies set by the mission area, science center, or equivalent unit may add more. Confirm with your supervisor rather than assuming.

Two files carry two states#

This is the part most easily got wrong, because it looks like a mistake when you see it in a diff.

File

On develop

On the release tag

DISCLAIMER.md

provisional wording (FSP §11)

approved wording (FSP §5)

code.json

version: "develop", status: "Development", versioned URLs → develop

version: "X.Y.Z", status: "Release", versioned URLs pinned to X.Y.Z

The approved disclaimer is introduced on the release-candidate branch, captured permanently in the tag, and never merged back to develop. CI enforces the trunk half of this: tests/test_release_metadata.py fails if approved wording reaches develop.

Both wordings come from the FSP Guidance on Disclaimer Statements, and each section is linked directly — copy from these rather than from anywhere else, including from this page:

A note on why develop reverts to provisional even though §5 says the USGS “reserves the right to update the software as needed pursuant to further analysis and review”: that clause scopes the warranty on the approved snapshot and makes future revisions subject to further review — it does not extend the approval to later commits. The first merge to develop after a release is unreviewed, so trunk must not claim an approval it does not hold.

Before you start#

  • Reserve a DOI through the USGS Asset Identifier Service. Do not activate it yet — activation happens after the GitLab Release exists, because the DOI must resolve to the Release. Some units have a designated person who creates DOIs; check first.

  • Create CITATION.md on develop carrying the reserved DOI. Do this only once the DOI is reserved — a file with a placeholder DOI is worse than no file.

  • Create the IPDS record.

  • Identify your administrative, code, and domain reviewers, and your approving official.

  • Confirm the engineering release this product is based on is already merged and green.

  • Confirm the tests-full job passed on the latest develop pipeline. It runs automatically there and its failure fails the pipeline, so this is a check rather than an action. On a merge request it is still a manual play button that never blocks. It runs the slow tests, which reach the NHGF STAC catalog, a THREDDS OPeNDAP endpoint and a parquet file on GitHub; those are deselected from every merge gate on purpose, because a third-party outage should not redden unrelated merge requests.

  • Write the release’s HISTORY.md section on develop, describing every user-visible change since that engineering release and labelling anything breaking. Do this on develop, not on the release-candidate branch: the candidate is never merged back, so an entry written there is lost. The published changelog {include}s this file, so an empty section means the documentation shows the previous release as the newest while PyPI serves this one.

The release-candidate branch#

The release-candidate branch is named exactly after the tag it will become — 0.4.1, not v0.4.1, not rc-0.4.1. It is created as an orphan branch so the review merge request shows the whole project as a reviewable diff.

git switch develop
git pull --ff-only origin develop

git switch --orphan 0.4.1
# If git asks "Deletion of directory 'NAME' failed. Should I try again?", answer n
git commit --allow-empty -m 'Create release candidate branch'
git push -u origin 0.4.1

# The orphan branch leaves you standing on an empty working tree -- go back.
git switch develop

Then open a merge request from develop into 0.4.1. This is the opposite direction from normal development — do not use the “Create merge request” button GitLab offers after the push, which would propose 0.4.1 develop. Create it manually and set the source and target explicitly.

Select the USGS_Release_Review description template. It is the course template already trimmed to this project, with separate sections for the administrative, code and domain reviewers, so nobody has to trim one under time pressure. Delete anything that genuinely does not apply to this release and say in its Notes what you deleted.

The upstream originals, if you need to compare:

Review and reconciliation#

The review must cover administrative, code, and domain aspects. The review template scopes a section to each, so a reviewer reads only their own.

Tell the administrative reviewer this explicitly, because a green pipeline invites the opposite conclusion: the trunk-state guards in tests/test_release_metadata.py stand themselves down on any ref named X.Y.Z — that is, on the release-candidate branch. That is deliberate, since the branch carries product state on purpose. But it means code.json’s pinned URLs and Release status, and the approved FSP §5 disclaimer, are not machine-checked where the reviewers are looking. Those are precisely the items that must be verified by hand. The template says so at the top of that section.

AI-assisted review is permitted provided a human reads the review and signs off on it. If you use it, record the sign-off explicitly on the checklist — who read it, and when. An unattributed AI review is not a review.

To reconcile:

  1. Make fixes on develop, not on the release-candidate branch.

  2. Push them. The merge request updates automatically, because develop is its source branch. Do not merge into the release-candidate branch to pick fixes up — that branch is the merge request’s target, and merging into it ends the review.

  3. Reply to each review thread with the commit hash that addressed it, then resolve the thread. Push the commit before posting the comment, or GitLab will not render the hash as a link.

  4. When every thread is resolved, expand them all and print the merge request to PDF. That PDF is the review and reconciliation artifact for IPDS.

Merging the review merge request#

Once the review is approved and every thread is resolved, merge the merge request into the release-candidate branch. The RC branch was created as an orphan branch holding a single empty commit; until this merge it has no project content at all, and a tag cut from it would archive an empty tree.

Everything in the sections below happens on the release-candidate branch after this merge.

Switching the branch to product state#

On the release-candidate branch only:

  • uv run bump2version <part> to set the product version, choosing whichever bump produces exactly the release-candidate branch name. The product version is normally a patch off the engineering release it is based on (e.g. 0.4.1 off 0.4.0), so patch is normally the right part. The branch is named after the tag it will become, so a bump that lands on any other number breaks that invariant. bump2version moves .bumpversion.cfg, src/gdptools/__init__.py, and docs/_config.yml together — do not hand-edit any of them.

  • Bump the gdptools==X.Y.Z pin in environment-examples.yml to the version being released. bump2version does not touch this file, and it does not track the package version automatically: it pins whatever release readers should install to run the example notebooks. It sat at 0.3.2 from 0.3.10 through 0.4.0, so anyone building that environment got a package predating the removal of the dask engine and the required calc_crs, while the notebooks in the repository assumed the current one. Pin the version being released even though conda-forge will not have it until the feedstock’s autotick bot lands — the file describes the release, and the environment is built after that package is available.

  • In code.json, set version to the release version and status to "Release"

  • In code.json, pin only the URLs that carry a version segment to the release version instead of develop: disclaimerURL, the license URL under permissions.licenses, and downloadURL. Leave homepageURL and repositoryURL exactly as they are — they are the bare repository URL and its .git form, with no version segment to pin. Mangling these two is the release validator’s most common rejection.

  • In code.json, replace the laborHours: 0 placeholder with the real figure — it ships into the permanent, citable product record

  • In code.json, refresh date.metadataLastUpdated to the date you are preparing the release

  • Replace DISCLAIMER.md with the approved FSP §5 wording

  • Re-confirm CITATION.md still carries the reserved DOI

The trunk-state guards in tests/test_release_metadata.py stand down automatically on a ref whose name is exactly a version number — which the release-candidate branch and its tag are — so a green pipeline is still expected here. They key on the ref, deliberately never on the file contents they check, so that drift on trunk cannot switch off the very guards that catch it. If they go red on the RC branch, the branch name and the product state disagree — fix the state, do not delete the guard.

Requesting publication#

Open an issue on the USGS GitLab Software Management queue and select the GitLab Official Release description template.

  • Replace the examples between backticks with your values.

  • Change [ ] to [x] to acknowledge each item.

  • Leave the /label lines alone — editing them delays processing.

  • Tag the approving official with @username, or give their email if they have no GitLab account.

An administrator runs an automated validation tool against the request. Its most common finding is incorrect URLs in code.json — which is why the trunk copy is tested in CI and the product copy is pinned deliberately.

Publishing#

Once the request is approved:

  1. Create the tag from the release-candidate branch, named identically to it.

  2. Create a GitLab Release from that tag. Put the recommended citation in the release notes. It lives in CITATION.md; copy it from there rather than retyping it. The form is:

    Author, A.B., Author, C.D., and Author, E.F., YEAR, gdptools,
    version X.Y.Z: U.S. Geological Survey software release,
    https://doi.org/10.5066/XXXXXXXX
    

    Note the punctuation: a comma before version, a colon after it. The year is the year the DOI is activated, which may not be the year the work was done. The Release, not the tag, is what the DOI points at — release notes can be edited later to redirect users to a newer version, and a tag cannot.

  3. Delete the release-candidate branch. A branch and a tag of the same name share a URL, so leaving both is confusing.

  4. Activate the DOI at the Asset Identifier Service, pointing it at https://code.usgs.gov/wma/nhgf/toolsteam/gdptools/-/releases/<version>. You will need the creators, publication year, that URL, the IPDS number, and the related publication if there is one. A description is required for the product to appear on the USGS website.

  5. Disseminate the record in IPDS, following your center’s policy.

  6. Publish to PyPI from the approved tag, so the citable product and the distributed artifact are the same code.

  7. Confirm the conda-forge feedstock updates. The feedstock’s bot notices the new sdist on PyPI and opens a pull request against conda-forge/gdptools-feedstock on its own; check that it appeared, that its dependency changes are right, and that it is merged. No manual upload is needed, but nobody is told if the bot’s pull request sits unmerged.

    Check the feedstock’s open issues before merging that pull request. Recipe cleanup is deliberately deferred to ride along with a version bump, rather than spending a build-number bump and a separate review on a test-only change — so the bot’s pull request is where those changes are meant to land. As of 0.4.0 there is one such issue, gdptools-feedstock#78, carrying five items. It lives on GitHub, in another repository, on a queue nobody on this side watches; this step is the only place it reliably resurfaces.

  8. Bring develop forward past the released version. The version bump happened on the release-candidate branch, which is never merged back, so develop and .bumpversion.cfg still read the previous version — and the next uv run bump2version patch on develop would regenerate a number already on PyPI, which uv publish rejects. Run uv run bump2version on develop to move it past the released version — that moves .bumpversion.cfg, src/gdptools/__init__.py, and docs/_config.yml together, and hand-editing any of them instead lands a red pipeline, because tests/test_version_consistency.py fails when they drift. Merge that through the normal process. The version-consistency tests cannot catch this: both files agree with each other, they just disagree with PyPI.

    Verify it rather than trusting the checkbox:

    uv run nox -s check-pypi-version
    

    That fails if PyPI holds a release newer than the version on the branch, and names the version and the fix. It skips rather than fails when PyPI is unreachable, and stands down on release tags and release-candidate branches, where the version is published on purpose. It is not part of the default test suite, because it needs the network. See issue #108.

References#