Skip to content

Releasing

How to cut a new jsonatapy/jsonata-core release.

Before dispatching

  1. Keep CHANGELOG.md's [Unreleased] section current as you go, not just at release time — add an entry under ### Added/### Changed/### Fixed/etc. as part of the PR that introduces the change, the same way you'd update tests. The release workflow enforces this: it refuses to release if [Unreleased] has no entries (see below), but a changelog written after-the-fact from a commit-message dump is much lower quality than one written when the change was still fresh in your head.
  2. Confirm cargo test --all-features, cargo clippy --all-targets --all-features -- -D warnings, cargo fmt --check, and the Python reference suite are all green on main.
  3. Decide the version number. This project's release versions track the jsonata-js major/minor version they target (patch numbers are independent — see the README) to reduce API-compatibility confusion for adopters. E.g. a release whose features/fixes target jsonata-js 2.2.x should itself be numbered 2.2.x, not a simple patch-increment of whatever the previous jsonatapy release happened to be.

Dispatching a release

Trigger the Release workflow manually (workflow_dispatch) from the Actions tab, or via gh workflow run release.yml -f version=X.Y.Z. Set dry_run: true first if you want to exercise the build/test/publish steps without actually publishing to PyPI/crates.io.

What the workflow does automatically

  • Validates the version string is well-formed semver.
  • Bumps Cargo.toml/pyproject.toml/Cargo.lock to the new version, and the jsonata-core = "…" install snippet in README.md (it warns, without failing, if that snippet no longer matches).
  • Bumps CHANGELOG.md: renames the accumulated ## [Unreleased] section to ## [$VERSION] - $DATE and inserts a fresh, empty [Unreleased] template above it (scripts/bump-changelog.sh). This step fails the release if [Unreleased] has no bullet entries — that's a strong signal the changelog wasn't kept current during development (see step 1 above). If you hit this and the release genuinely has nothing user-facing to note, add a bullet by hand (e.g. "Internal maintenance release, no user-facing changes.") rather than bypassing the check.
  • Commits the version bump (including the updated CHANGELOG.md and README.md) and creates the vX.Y.Z tag — idempotently: re-dispatching the same version after a partial failure reuses the existing commit/tag/changelog entry instead of duplicating them, but fails loudly (does not silently proceed) if the tag already exists at a different commit than the one this run would release, since that's previously caused stale-commit bugs in benchmark/build jobs that all check out refs/tags/v$VERSION explicitly.
  • Builds wheels for all target platforms, builds the sdist, runs the test suite against the built wheels, and (unless dry_run: true) publishes to PyPI and crates.io, then creates the GitHub Release. The release notes are this version's CHANGELOG.md section (scripts/release-notes.sh, with empty subheadings dropped), followed by a link to the full commit comparison and the install instructions. What you write in the changelog is what readers see on the release page, so write it for them. The notes are generated before anything is published, and the release fails if the section is missing or empty.
  • Runs the performance benchmark suite on the self-hosted Mac Mini runner (not a GitHub-hosted runner — single-sample wall-clock benchmarks on shared/virtualized cloud runners are dominated by noise, confirmed empirically: identical code measured twice swung -66% to +120%), records the results to the benchmark-data branch under results/macos-arm64/, regenerates docs/performance.md from this release's numbers and commits it to main, and opens an issue if any benchmark regressed more than 10% versus the previous release.
  • This runner needs Node.js manually provisioned once (scripts/setup-macos-benchmark-runner.sh, run directly on the runner host, not in CI) — everything else (Python via uv, the Rust toolchain) is installed fresh by the workflow's own GitHub Actions on every run. See that script's comments for why (sudo/Homebrew weren't available, so it installs Node as a plain user-space binary and extends the runner's own .path file instead).
  • A separate, PR/push-triggered workflow (.github/workflows/benchmark.yml) runs on ubuntu-latest and maintains its own, separate baseline under results/ubuntu-latest/ — intentionally decoupled from this job's macOS baseline, since comparing a ubuntu-latest PR run against a Mac Mini release baseline would be exactly the apples-to-oranges mismatch this setup exists to avoid.

After a release

  • Verify the new version on PyPI/crates.io and that import jsonatapy; print(jsonatapy.__version__) reports the expected version.
  • If anything about the release process itself needs fixing, that's a .github/workflows/release.yml change like any other — it's not a separate, undocumented process.