Releasing
August 25, 2026 · View on GitHub
Every artifact in this repo shares one version and releases in lockstep, so a given version number means the same underlying Rust engine everywhere.
Versioning model (see DISTRIBUTION.md §2.2 for the rationale):
- The Cargo
[workspace.package]version (rootCargo.toml) is the engine version. The core crate, CLI, Python wheel, R package, and the Java FFM binding all carry this same number. org.gbif:name-parser-api(currently5.0.1) is the stable Java contract — an independently versioned dependency, not part of this version. The Java binding implements it but versions with the engine (an impl versioning independently from its api is normal).- The bindings sit at
0.xwhile new and gathering real-use feedback. Once stable they graduate to the product's5.xline — a deliberate one-time re-baseline, in lockstep.
| Channel | Artifact | Registry | Trigger | Status |
|---|---|---|---|---|
| Java FFM binding | org.gbif.nameparser:name-parser-rust | GBIF Nexus | Jenkins | ✅ 0.2.0 released |
| CLI | nameparser-cli-<target> archives | GitHub Releases | cli-v* tag | ✅ 0.2.0 released |
| Python | gbif-name-parser | PyPI | py-v* tag | ✅ 0.2.0 published (setup done); later versions via tag |
| Rust engine | gbif-name-parser | crates.io | crate-v* tag | ✅ 0.2.0 published (setup done); later versions via tag |
| R | nameparser | CRAN | manual submission | ⚠️ not yet wired |
0. One-time setup
Do these once (per registry / per person with release rights).
- PyPI (Trusted Publishing — no token stored). ✅ DONE. The GitHub
pypiandtestpypienvironments exist (repo Settings → Environments;pypiis gated behind a required reviewer), the Trusted Publisher is registered (ownergbif, reponame-parser-rust, workflowpython-release.yml, environmentpypi), andgbif-name-parser0.1.0is on PyPI — so later versions publish token-free from apy-v*tag.- Dry-run channel: the same registration on https://test.pypi.org with environment
testpypi, driven by the workflow's "Dry run … TestPyPI" input.
- Dry-run channel: the same registration on https://test.pypi.org with environment
- crates.io (Trusted Publishing — no token stored). ✅ DONE. The GitHub
crates-ioenvironment exists (gated behind a required reviewer),gbif-name-parser0.1.0is published, and its Trusted Publisher is registered (ownergbif, reponame-parser-rust, workflowcrate-release.yml, environmentcrates-io) — so later versions publish token-free from the workflow.- How the first publish was bootstrapped (for the record — unlike PyPI, crates.io has no
pending-publisher flow, so the crate must exist before a Trusted Publisher can be attached):
create a short-lived scoped API token (crates.io → Account Settings → API Tokens, scope
publish-new), runCARGO_REGISTRY_TOKEN=<token> cargo publish -p gbif-name-parseronce to create the crate, add the Trusted Publisher on the now-existing crate's Settings, then revoke the token.
- How the first publish was bootstrapped (for the record — unlike PyPI, crates.io has no
pending-publisher flow, so the crate must exist before a Trusted Publisher can be attached):
create a short-lived scoped API token (crates.io → Account Settings → API Tokens, scope
- Jenkins (Java). The Multibranch job already deploys snapshots. Release credentials
(
gbif-release/gbif-snapshot) live only in the Jenkins-managedsettings.xml— never in the repo. - CRAN (when enabling — see §2): a maintainer email + the manual submission form.
1. Bump the version (always first)
scripts/bump-version.sh 0.2.0 # Cargo workspace + pyproject + DESCRIPTION + R crate + pom (X-SNAPSHOT)
# + the pom's <rust.engine.version> (stamped into the JAR manifest)
git diff # sanity-check: only the version fields changed
Test, then commit and push:
cargo test --workspace --exclude nameparser-py # py needs maturin, not plain cargo
cargo build -p nameparser-ffi --release # the cdylib the Java tests load
mvn -f bindings/java/pom.xml test # ParityTest 8,017/0 + smoke
git add -A && git commit -m "Release 0.2.0" && git push
The Java pom carries
0.2.0-**SNAPSHOT**(Maven dev-version convention); the Jenkins release job strips-SNAPSHOTto0.2.0at release time, so it lands on the same number as the others.
2. Release each channel
The channels are independent — release any subset. For a full coordinated release, do them all at the bumped version.
Java → GBIF Nexus (Jenkins)
- Snapshots: every push to
mainauto-deploysX-SNAPSHOT(parity runs in CI). Nothing to do. - Release
X: run the Jenkins job withRELEASE=trueonmain(optionally setRELEASE_VERSION/DEVELOPMENT_VERSION). It runsrelease:prepare release:perform, taggingvXand deploying the release + per-arch classifier JARs. Run the first release as a dry-run (-DdryRun=true) to confirm end to end — see theJenkinsfilerelease stage. - Afterwards: maven-release-plugin bumps
bindings/java/pom.xmlto the next dev version but does not touchbindings/java/jmh/pom.xml, which is a standalone module outside its reactor. Point that module'sname-parser-rustdependency at the new dev version — the snapshot it named before is consumed by the release and 404s on Nexus.
CLI → GitHub Releases
git tag cli-v0.2.0 && git push origin cli-v0.2.0
.github/workflows/cli-release.yml builds nameparser-cli natively on 4 targets (linux
x86_64/aarch64, macOS arm64, windows x64) and attaches per-platform archives + SHA-256 to
the cli-v0.2.0 release. (Intel macOS is not built — GitHub is retiring the macos-13 Intel
runners; Intel-Mac users build from source.)
Python → PyPI
Dry-run first (recommended): Actions → "Publish Python" → Run workflow from main with the
"Dry run … TestPyPI" box checked → builds all wheels + sdist and publishes to TestPyPI. Verify:
pip install -i https://test.pypi.org/simple/ gbif-name-parser
Then the real release:
git tag py-v0.2.0 && git push origin py-v0.2.0
.github/workflows/python-release.yml builds the wheels (abi3 → one per platform, CPython 3.9+) +
sdist and publishes to PyPI via Trusted Publishing. A guard fails the run if the tag doesn't match
pyproject.toml's version (PyPI uploads are irreversible). Result: pip install gbif-name-parser.
Rust engine → crates.io
gbif-name-parser 0.1.0 is published (that first version was bootstrapped manually — see §0);
subsequent versions publish via Trusted Publishing (OIDC — no stored token). crates.io must precede
a CRAN release — the R package vendors the core from crates.io.
For any subsequent release, after the §1 version bump — dry-run first (recommended):
Actions → "Publish crate" → Run workflow → runs cargo publish --dry-run (packages + verifies,
never publishes). Then the real release:
git tag crate-v0.2.0 && git push origin crate-v0.2.0 # NOT crate-v0.1.0 — 0.1.0 is already published
.github/workflows/crate-release.yml guards the tag against the engine version (root Cargo.toml
[workspace.package]), authenticates via OIDC (rust-lang/crates-io-auth-action), and runs
cargo publish -p gbif-name-parser. The crates-io GitHub environment is gated behind a required
reviewer, so the run pauses for approval before the (irreversible) upload.
(nameparser-cli/-ffi/-py stay publish = false — they are not library crates.) The one-time
crates.io Trusted-Publisher registration is already done (§0).
R → CRAN — needs crates.io first
CRAN is source-based and human-reviewed (no auto-publish). Prerequisite: the core on crates.io
(above — now satisfied, 0.1.0 is published) — the R package isn't self-contained until it depends
on gbif-name-parser by version and can vendor it (cargo vendor skips local path deps). Then:
- Point
bindings/r/src/rust/Cargo.toml'snameparser_coredependency at the crates.io version. Rscript -e 'rextendr::vendor_pkgs("bindings/r")'→ bundles the core + all deps intosrc/rust/vendor.tar.xz(theMakevarsalready builds offline from it).- Add the vendored-crate license inventory (
inst/AUTHORS) CRAN requires for bundled sources. R CMD check --as-cranuntil clean, then submit the source tarball via https://cran.r-project.org/submit.html.
Release checklist (copy per release)
[ ] scripts/bump-version.sh X → review git diff → test → commit → push
[ ] Java: Jenkins job (RELEASE=true) — or snapshot-only if not cutting a release
[ ] CLI: git tag cli-vX && git push origin cli-vX → verify the GitHub release assets
[ ] Python: TestPyPI dry-run → git tag py-vX && git push → verify `pip install gbif-name-parser`
[ ] crates.io: dry-run → git tag crate-vX && git push (CRAN: still unwired, see §2)
[ ] Confirm all published artifacts report version X (same engine everywhere)
[ ] Flip the "published at <version>" claims to X: README.md (status banner + binding table),
DISTRIBUTION.md §2 table. Everything else is bumped by scripts/bump-version.sh at §1.