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.
  • 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 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 with autogenerated notes.
  • 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.