NautilusTrader
Developer Guide

Releases

This guide covers the release process and the standards for writing release notes.

Overview

NautilusTrader uses a three-branch model:

  • develop: active development. Publishes dev wheels to Cloudflare R2 on each push.
  • nightly: pre-release testing. Publishes all supported pre-release wheels and CLI binaries.
  • master: stable releases. Triggers the full release pipeline.

When a release commit merges to master, the pipeline automatically:

  • Tags the version from python/pyproject.toml.
  • Creates a draft GitHub release.
  • Uploads release assets.
  • Publishes Cargo crates to crates.io.
  • Publishes wheels and sdist to PyPI.
  • Publishes the GitHub release.
  • Builds Docker images.
  • Triggers a docs rebuild.

Stable release workflow

The build workflow treats the GitHub release as the anchor for stable releases. It first creates a draft release. It uploads the wheel and sdist assets to that draft. Only then does it publish those packages to package indexes. The workflow publishes the GitHub release only after the registry verification and final integrity assets are complete.

Keep these sequencing rules intact when editing .github/workflows/build.yml:

  • The draft GitHub release must exist before any release asset upload or package registry publish.
  • tag-release must depend on security-audit so stable release tagging cannot proceed after an audit failure.
  • Wheel and sdist assets must be attached to the GitHub release before package index publishing starts (packages.nautechsystems.io, PyPI, crates.io).
  • PyPI and crates.io Trusted Publishing jobs must keep environment: release and id-token: write. Those registrations depend on the release environment.
  • Non-OIDC integrity and asset-upload jobs should avoid environment: release unless they need release environment secrets or approvals.
  • publish-release-integrity must run after PyPI and crates.io publishing. It generates the release manifest first, verifies registries against that manifest, then attaches final integrity assets only after verification passes.
  • publish-github-release must be the final stable release job. GitHub recommends creating a draft release, attaching all assets, then publishing the draft before enabling release immutability. If GitHub release immutability is enabled for the repo, published release assets and the release tag cannot change. Only the title and release notes stay editable. The job verifies the final draft asset set before publishing and verifies GitHub's release attestation after publishing the draft.

Recovering a missing release tag

The Create git tag step creates v<version> only when the run's commit changes the version from its parent (HEAD~1), and never when the tag already exists. If the version is unchanged and the tag is missing, Verify release tag fails no matter how often the failed jobs are re-run.

A retried release normally has one or more commits on master after the version bump. Thus, the retry run has the same version as its parent and does not create the tag. Because tag-release anchors the draft release and each downstream publish job, the run cannot proceed while the tag is missing.

First check whether the tag exists and whether the run's commit bumps the version:

  • If the tag is missing and the run's commit bumps the version, rerun the failed jobs. No manual tag is necessary. After the jobs pass, tag-release creates the tag.
  • If the tag is missing and the run's commit carries the same version as its parent, no re-run creates the tag. Recover with the manual tag procedure below.

Manual tag recovery works because of the remaining tag-release mechanics:

  • On a re-run after the manual push, Create git tag skips creation under the same rule.
  • Verify release tag requires v<version> to resolve to the run's commit (GITHUB_SHA), so a manually pushed tag at that exact commit passes the check.
  • Draft release creation is idempotent, so a re-run updates the existing draft if one was already created.

To recover, a maintainer creates the tag manually and re-runs the failed jobs:

  1. From the failed run page, note the run's commit SHA. Check out that commit locally and confirm the version it carries:

    ./scripts/package-version.sh
  2. Create a signed annotated tag at the run's exact commit, using the existing tag message convention, and push it:

    git tag -s v<version> -m "Released version <version>" <run-commit-sha>
    git push origin v<version>
  3. Verify the tag locally. The signature must be good and git rev-parse 'v<version>^{commit}' must equal the run's commit SHA:

    git tag -v v<version>
    git rev-parse 'v<version>^{commit}'
  4. On the failed run, select "Re-run failed jobs". Verify release tag passes with the pushed tag, and draft release creation and the downstream publishing jobs continue.

Only do this when the release commit and version are final. The tag is the permanent release anchor, and once release immutability applies the published release tag cannot be changed. If v<version> points at a different commit, do not move or re-push the existing tag. Investigate the mismatch. Verify release tag fails in that case by design.

Recovering a partial release

Use the recover-release workflow when packages already exist on PyPI and crates.io but release integrity verification or final GitHub publication fails. It verifies existing packages without rebuilding or republishing them. It also verifies a release that a maintainer has already published manually.

The source must be a completed build workflow run from a push to master in this repository, with a successful tag-release job. The release tag must resolve to that run's commit, and the tag version must match python/pyproject.toml at that commit. Recovery checks out that source and requires the release's complete package inventory to match the current stable wheel matrix, including the matching sdist. Historical releases with a different matrix fail verification.

  1. Wait for the original build and any other recovery attempt to finish. Do not rerun the source build during recovery. Jobs that write the tag or GitHub release share a lock by release tag. GitHub permits one pending job per lock; a newer pending job replaces the earlier pending job.
  2. Open Actions > recover-release > Run workflow and select master.
  3. Enter the existing release tag and the original build run ID, then choose a mode:
    • verify: require an already-published release and check it without changing its assets, notes, or publication state.
    • recover: resume a draft release. If it is already published, run verification without changing it.
  4. Review the recovery artifact even when the workflow fails. After correcting the cause, rerun with the same tag and source run ID.

Recovery verifies package attestations and their sibling bundles against the original build workflow and source commit, checks each envelope against its verified bundle, then verifies registry contents. It compares existing GitHub assets by content, uploads only missing assets, and stops on a mismatch. Existing dist-manifest.json and crates-manifest.json assets keep their bytes when their JSON content matches apart from generated_at. Recovery updates checksum notes only when needed, verifies the complete draft asset set, checks Rekor readiness, publishes the draft, and verifies GitHub's release attestation.

Recovery restores missing packages or provenance only from unexpired artifacts belonging to the selected source run. If required evidence is unavailable, recovery stops. A fully populated published release does not require retained source-run artifacts. After a failure that follows publication, rerun either mode to check the release again without republishing it.

If an asset upload fails, recovery stops after that attempt. Rerun recovery to verify the assets that arrived and upload only the remaining files. An interrupted upload can leave an incomplete asset or a file whose content does not match. Recovery names the affected asset and stops. Confirm that the release is still a draft and that the named asset came from the interrupted upload. Delete only that confirmed bad asset from the draft, then rerun recovery. Do not delete or replace assets on a published release.

The runner's GitHub CLI must support gh attestation verify --source-digest and gh release verify. Recovery checks both capabilities before changing the release. The workflow retains source metadata, tool versions, the last step checkpoint, per-step and retry logs, exit status, registry responses, downloaded verification inputs, and the isolated verifier cache for 30 days. The normal build's integrity and final publication jobs also retain diagnostic artifacts on failure. A later successful rerun preserves those artifacts.

Versioning

The project maintains two version numbers:

FileScope
python/pyproject.tomlPython package
Cargo.toml (workspace)Rust crates

These are bumped independently. The Python version drives the v<python-version> release tag. Versions ending in aN, bN, or rcN create a GitHub pre-release. Final versions create a normal release.

Crates.io publishing

The build workflow publishes Cargo crates from the publish-cargo-crates job. The job uses crates.io Trusted Publishing through GitHub Actions OIDC, so it does not use a persistent cargo token. Configure each crate on crates.io with:

FieldValue
Ownernautechsystems
Repositorynautilus_trader
Workflowbuild.yml
Environmentrelease

Enable Trusted Publishing Only for crates after their trusted publisher is configured. Crates that have never been published still need an initial manual publish before crates.io allows the trusted publisher configuration.

Do not use cargo publish --workspace for CI releases. The release job runs scripts/ci/publish-cargo-crates.sh, which:

  • Publishes crates one at a time in dependency order.
  • Skips versions already present on crates.io.
  • Waits for each new version to appear in the crates.io API and sparse index before publishing dependents.

The script fails before uploading if a publishable crate depends on a local publish = false crate absent from crates.io. Optional local dependencies also block publication because a public feature that resolves to an absent crate would be unusable.

Post-publish verification treats an existing crate version as previously_published only when crates.io shows it was trusted-published by this repository.

Manual recovery: User-published versions fail verification unless CRATES_IO_MANUAL_PUBLISH_EXCEPTIONS names each recovered crate@version for emergency token-publish recovery. Accepted entries appear in crates-manifest.json with release_status: "manual_token_publish". Malformed or unused exception entries fail the job.

Wrong trusted-publishing repositories and checksum or sparse-index mismatches also fail verification.

Release checklist

Pre-release (on develop)

  • Finalize RELEASES.md: review all items, remove empty sections
  • Ensure versions are set in python/pyproject.toml and the Cargo.toml workspace
  • Ensure crates.io Trusted Publishing is configured for every crate that CI publishes: bash scripts/ci/check-crates-io-trusted-publishing.sh
  • Run make check-features locally on the final release candidate
  • Run make check-docsrs locally on the final release candidate
  • Ensure all CI checks pass on develop

Release

  • Merge develop into nightly, verify nightly CI passes
  • Merge nightly into master
  • Verify the build workflow completes:
    • Wheels built for Linux x86/ARM, macOS, Windows
    • cargo-deny and cargo-vet pass
    • security-audit passes its Zizmor and supply-chain checks
    • Release docs/features and Cargo publish preflights pass before tagging
    • Tag and draft GitHub release created
    • Wheels and sdist attached to the GitHub release before package registry publishing
    • Cargo crates published to crates.io or skipped because the version already exists
    • Wheels and sdist published to PyPI
    • Registry verification passes before release checksums, crates manifest, and attestation siblings are attached
    • GitHub release published after all release assets and integrity assets are attached
    • If the run fails with the release tag missing, follow Recovering a missing release tag and re-run the failed jobs
    • If packages exist but integrity verification or GitHub publication fails, follow Recovering a partial release
  • Verify the docker workflow completes (images built and pushed)
  • Verify the build-docs workflow completes (docs rebuild triggered)

Post-release (on develop)

  • Update the release date in RELEASES.md for the published version
  • Add horizontal separator --- below the completed release
  • Add the next version template at the top of RELEASES.md (see below)
  • Bump python/pyproject.toml version to the next release number
  • Bump crate versions in tutorial and how-to Cargo.toml snippets (docs/concepts/rust.md, docs/how_to/run_rust_backtest.md, docs/how_to/run_rust_live_trading.md)

Release notes

This section documents the standards for writing release notes in RELEASES.md.

Sections

Use the following sections in this order:

  1. Enhancements
  2. Breaking Changes
  3. Security
  4. Fixes
  5. Internal Improvements
  6. Documentation Updates
  7. Deprecations

Omit sections that have no items for a given release.

Enhancements

New features and user-visible improvements.

Format:

- Added `subscribe_order_fills(...)` and `unsubscribe_order_fills(...)` for `Actor`
- Added Bybit conditional orders support
- Added support for `OrderBookDepth10` requests (#2955), thanks @faysou

Guidelines:

  • Start with "Added".
  • Use backticks for code elements.
  • Be specific about what was added, not how.

Breaking Changes

Changes that may break existing code.

Format:

- Removed `nautilus_trader.analysis.statistics` subpackage - must import from `nautilus_trader.analysis`
- Renamed `BinanceAccountType.USDT_FUTURE` to `USDT_FUTURES`
- Changed `start` parameter to required for `Actor` data request methods

Guidelines:

  • Start with "Removed", "Renamed", or "Changed".
  • Explain migration path briefly.

Security

Security hardening and fixes that prevent crashes, undefined behavior, or data corruption. Includes significant hardening improvements elevated from Internal Improvements.

Format:

- Fixed divide-by-zero and overflow bugs in model crate that could cause crashes
- Fixed core arithmetic operations to reject NaN/Infinity values and improve overflow handling

Guidelines:

  • Include overflow/underflow fixes, memory safety improvements, FFI guards, data integrity fixes.
  • Focus on user impact: what could have happened.
  • Exclude routine dependency updates, minor hardening, or test-only fixes.
  • Omit this section entirely if there are no security items for the release.

Fixes

Bug fixes that improve correctness but do not qualify as security issues.

Format:

- Fixed reduce-only order panic when quantity exceeds position
- Fixed Binance order status parsing for external orders (#3006), thanks for reporting @bmlquant

Guidelines:

  • Start with "Fixed".

Internal Improvements

Implementation details and infrastructure changes.

Format:

- Added ARM64 support to Docker builds
- Ported `PortfolioAnalyzer` to Rust
- Improved clock and timer thread safety
- Upgraded Rust (MSRV) to 1.90.0
- Upgraded `pyo3` crates to v0.26.0

Guidelines:

  • Use "Added", "Implemented", "Improved", "Optimized", "Upgraded", "Refined", "Standardized".
  • Include version numbers for dependency upgrades.

Documentation Updates

Changes to guides and examples.

Format:

- Added rate limit tables with links to official docs
- Improved dark and light themes for readability
- Fixed broken links

Deprecations

Features marked for removal.

Format:

- Deprecated `some_config_option`; disable (`False`) to maintain consistent behavior. Will be removed in future version

Guidelines:

  • Explain migration path and provide alternatives.

Attribution

  • Credit external contributors: thanks @username or thanks for reporting @username.
  • Include issue/PR numbers for community contributions and complex features: (#1234).

Style

  • Use sentence case (capitalize first word only).
  • Do not end with periods.
  • Use backticks for code elements.
  • Focus on what changed, not how.

Be specific:

Bad:  Improved Binance adapter
Good: Improved Binance fill handling when instrument not cached

Security classification

Include in Security if the change addresses:

  • Memory safety (overflow, underflow, divide-by-zero that threatens stability).
  • Undefined behavior or crashes that could corrupt state.
  • Data integrity (NaN/Infinity propagation, race conditions leading to corruption).
  • Input validation preventing injection or exploitation (SQL injection, command injection, path traversal).
  • Build hardening (non-exec stack, FFI guards).
  • Significant hardening that users should know about.

Otherwise use Fixes (for logic bugs and panics) or Internal Improvements (for minor hardening).

Note: Plain logic panics belong in Fixes unless they threaten system stability or data corruption.

Examples

Security (could cause crashes/corruption):

- Fixed divide-by-zero in margin calculations that could crash the engine
- Fixed integer overflow in model arithmetic that could crash the process

Fixes (incorrect but safe):

- Fixed Binance order status parsing for external orders
- Fixed position purge logic to prevent purging re-opened position

Enhancements (user-facing):

- Added Bybit conditional orders support

Internal (implementation):

- Implemented Bybit ping/pong handling

Release notes template

# NautilusTrader <VERSION> Beta

Released on TBD (UTC).

### Enhancements

### Breaking Changes

### Security

### Fixes

### Internal Improvements

### Documentation Updates

### Deprecations

---

On this page