Release Process

rgpot uses a lockstep monorepo version: the same SemVer appears in meson.build, CMakeLists.txt (project(VERSION ...)), rgpot-core/ Cargo.toml, towncrier.toml, and pixi.toml (workspace key only — not dependency pins such as nickel). Git tags are vX.Y.Z (see cog.toml tag_prefix). Pre-release tags such as v1.2.0-rc.1 are allowed; release.yml marks the GitHub Release as prerelease and skips cargo publish (stable crates.io only).

Requires cocogitto 7+ locally and in CI (release-prepare.yml installs 7.0.0), plus a normal Rust toolchain so potctl (potctl/ workspace crate; cargo run -p potctl -- ) can run lockstep commands. CI builds target/release/potctl in release.yml / release-prepare.yml. Do not hand-edit a ## [X.Y.Z] section above the towncrier marker; fragments under docs/newsfragments/ are the only input for the next release notes block.

Canonical changelog: repository-root CHANGELOG.md (towncrier). docs/orgmode/changelog.org only includes that file for Sphinx; do not maintain a second prose changelog.

Tooling:

Tool

Role

cocogitto (cog) 7+

Conventional-commit aware bump, version commit, annotated tag

towncrier

Curated CHANGELOG.md from docs/newsfragments/

potctl (potctl/, publish = false)

Project CLI: release sync / release assert / release notes

.github/actions/setup-ci-tools

Build potctl (+ optional cog) once with Swatinem/rust-cache; optional artifact upload

.github/actions/restore-ci-tools

Later jobs/runners: download ci-tools artifact, put bin/ on PATH (no second cargo build)

.github/workflows/release.yml

Staged on v*: toolsgate → (cpp-smokerust-test) → publishgh-release

.github/workflows/release-prepare.yml

Staged: tools (potctl=+=cog artifact) → prepare (lockstep, towncrier, cog dry-run)

CI (orchestrator) / towncrier

towncrier check on PRs (orchestrator job)

CI (orchestrator) / potentials

Feature-gated C++ backends (xtb+tblite, metatomic/vesin) on PRs

SECURITY.md

Supported versions + private report path

.github/CODEOWNERS

Release-surface review hints (enable on branch protection)

Secrets / protection (maintainer, once per repo):

  • Repository secret CARGO_REGISTRY_TOKEN for release.yml cargo publish.

  • Optional GitHub Environment named release with required reviewers (workflow

references environment: release; create under Settings > Environments).

  • Prefer signed tags when cutting locally: git tag -s vX.Y.Z (or ensure cog

produces signed tags if configured). GHA does not replace maintainer GPG.

C++ distribution note: tag/source is the C++ contract; release.yml runs a minimal meson install smoke. Optional backends (xtb/tblite/metatomic) are validated on PRs via orchestrator potentials jobs, not re-run on every tag. conda-forge / Spack / system packages are out of band (separate feedstock/PR track), not part of release.yml.

Changelog fragments

Each user-facing PR should add a fragment under docs/newsfragments/ (one fragment per user-visible change; see docs/newsfragments/README.md):

uvx towncrier create --content "Short user-facing note." 42.added.md
# or interactive:
uvx towncrier create

Types (see towncrier.toml): security, removed, deprecated, added, dev, changed, fixed, misc. Preview with uvx towncrier build --draft or pixi r towncrier-draft if defined.

CI runs towncrier check on pull requests when shipped paths change (orchestrator towncrier runs on all PRs; fragments under CppCore/, rgpot- core/, meson, CMakeLists.txt, pixi.toml, release scripts, etc.). Pure docs / ci / chore commits may omit fragments if they avoid those paths.

Preparing a release (maintainer) — run as soon as this lands on main

  1. Ensure main is green (orchestrator CI gate, potentials if

relevant).

  1. Confirm fragments exist for everything since the last tag (=uvx towncrier build

–draft=).

  1. Local sanity:

    cargo run -q -p potctl -- release assert
    cargo publish --manifest-path rgpot-core/Cargo.toml --locked --dry-run
    
  2. Dry-run bump (optional; also GHA workflow_dispatch on release-prepare.yml):

    cog bump --auto --dry-run
    # or: --patch / --minor / --major / --version 1.2.0
    
  3. Cut the release locally (needs push rights; does not publish to crates.io by

itself — the tag workflow does):

# Stable 1.2.0 example (first cut after lockstep PR merges with versions at 1.2.0):
cog bump --version 1.2.0
# or let cog compute from conventional commits since last tag:
# cog bump --auto
git push origin HEAD --follow-tags

Hooks (cog.toml pre_bump_hooks):

  • cargo run -q -p potctl -- release sync {{version}} — lockstep write (meson,

CMake, towncrier, Cargo, pixi workspace)

  • uvx towncrier build --yes --version {{version}} — update CHANGELOG.md, clear

fragments

  • cargo run -q -p potctl -- release assert --require-changelog {{version}}

— gate

  1. Tag push triggers release.yml (staged, not one fat job):

    • tools job: setup-ci-tools builds potctl (rust-cache shared-key =potctl-

ci-tools=) and uploads artifact ci-tools-<sha>

  • gate job: restore-ci-tools only — =potctl release assert –require-

changelog=, release notes → artifact release-notes-<sha> (fail fast before matrix work)

  • cpp-smokerust-test (parallel; only need green gate)

  • publish (stable tags only) then gh-release (body from gate artifact; RC

prerelease)

Within a single job, steps share the runner FS (no reinstall per step). Across jobs, binaries travel via artifacts; cargo=/deps hit =Swatinem/rust- cache on tool and rgpot-core runners so the next tag cut is cheap when sources are unchanged.

Do not run cargo publish by hand unless CI is unavailable; the workflow is the source of truth. Do not use cargo publish without --locked (lockfile drift must fail the release).

Pre-release / RC tags

cog bump --version 1.2.0-rc.1
git push origin HEAD --follow-tags
  • Creates v1.2.0-rc.1, builds towncrier section for 1.2.0-rc.1 if fragments

exist.

  • release.yml creates a prerelease GitHub Release and does not publish to

crates.io (avoid yanking/stable pollution).

  • Promote by a normal stable bump to 1.2.0 with a full fragment set if needed.

Version policy

  • Lockstep: C++ (meson + CMake), Rust crate (rgpot-core), docs towncrier

stamp, and pixi workspace version share one semver (optionally with -[a-z0- 9.]+ suffix for RCs).

  • One global tag: vX.Y.Z or vX.Y.Z-rc.N (not rgpot-core-v…).

  • Crate publish ships rgpot-core only on stable tags; C++ consumers track

the git tag / source tarball, not crates.io.

Emergency / hotfixes

cog bump --patch
git push origin HEAD --follow-tags

For a named version:

cog bump --version 1.1.1

Local checks before bump

pixi r rust-test-all
pixi r prek
uvx towncrier build --draft
cargo run -q -p potctl -- release assert
cargo run -q -p potctl -- release sync 9.9.9   # ok semver; use real target in dry tests
# refuse garbage:
cargo run -q -p potctl -- release sync not-a-version   # must error
# restore if you ran a fake version: git checkout -- meson.build CMakeLists.txt ...
cargo publish --manifest-path rgpot-core/Cargo.toml --locked --dry-run
pixi r lychee-check   # optional link hygiene

Post-merge checklist (first 1.2.0 cut)

  1. Merge the lockstep/release-machinery PR to main (versions may already read

1.2.0).

  1. Ensure rustc / cargo work (same as building rgpot-core).

  2. On main: cog bump --version 1.2.0 (or --auto if you want cog to decide).

This runs potctl release sync, towncrier build (so CHANGELOG.md gains ## [1.2.0] and fragments are cleared), then potctl release assert --require- changelog.

  1. git push origin main --follow-tags (or push commit then v1.2.0 explicitly).

  2. Confirm Release workflow: lockstep gate, smoke install, rust tests, crates.io,

GH Release body.

  1. Optional: announce; open conda-forge/Spack bumps out of band if applicable.