Skip to content

ci: release pipeline with abi3 wheels, smoke tests and trusted publishing (#60) - #232

Merged
Sean-Koval merged 2 commits into
mainfrom
ci/60-release-pipeline
Oct 1, 2026
Merged

Sean-Koval merged 2 commits into
mainfrom
ci/60-release-pipeline

Conversation

@Sean-Koval

@Sean-Koval Sean-Koval commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Refs #60. Not Closes: the first publish still needs the distribution name (#58) and the one-time maintainer setup below.

What changed

.github/workflows/release.yml ("Release Readiness") is replaced by a release pipeline:

Job Tag push v* workflow_dispatch (dry_run, default true) PR touching packaging files
metadata: reads name/version/toolchain; the three manifest versions must agree; tag must be v<version> yes yes yes
release-notes: section of CHANGELOG.md (required on a publish; dry runs fall back to Unreleased) yes yes yes
release-check (fmt, clippy, fast tests, benches compile) + release-slow-tests (SADF) yes yes no (ci.yml covers them)
crate-package: cargo package --list + cargo publish -p openquant --locked --dry-run yes yes yes
wheels: maturin-action, linux x86_64 + aarch64 (manylinux_2_28, native arm runner), macOS x86_64 + arm64, windows x86_64 yes yes yes
smoke: each wheel on a fresh runner, clean venvs for Python 3.11, 3.12, 3.13, runs the Quickstart (scripts/release/smoke_wheel.py) yes yes yes
sdist: built, then a wheel built from the sdist alone, installed and smoke-tested yes yes yes
publish-pypi (trusted publishing, pypi environment, id-token: write) yes only dry_run=false on a tag ref no
publish-crate (cargo publish -p openquant, CARGO_REGISTRY_TOKEN) yes same no
github-release (notes from CHANGELOG, wheels + sdist attached) yes same no

Publish jobs depend on every build/test job. A dispatch with dry_run=false on a branch fails in metadata.

The distribution name lives only in pyproject.toml. scripts/release/release_info.py metadata reads [project] name; the workflow uses that output (environment URL, smoke test's importlib.metadata check) and never spells the name out. Choosing the name in #58 is a one-line change to pyproject.toml plus the trusted-publisher entry. The pyopenquant placeholder was left as is.

abi3: [tool.maturin] features = ["pyo3/abi3-py311"] in pyproject.toml, so there is one cp311-abi3 wheel per platform. PyO3 0.29 and pyo3-polars 0.28 use only the limited API (pyo3-polars goes through Python polars' Series._export/_import and PyCapsule_Import), and the bindings use no non-limited API. The feature is set for maturin only, so cargo test --workspace and clippy still build against the full API. The smoke test checks the installed extension really is the abi3 build (_core.abi3.so / _core.pyd) and loads on 3.11, 3.12 and 3.13.

Toolchain/maturin pins: the Rust toolchain is read from rust-toolchain.toml (1.98.1) and passed to maturin-action and dtolnay/rust-toolchain; maturin is pinned to v1.15.0 (the version in uv.lock; pyproject requires >= 1.9.4).

New files: scripts/release/release_info.py (version checks + changelog notes extraction, stdlib only), scripts/release/smoke_wheel.py (the Quickstart against an installed wheel), python/tests/test_release_info.py (14 tests).

Docs: docs/publishing.md rewritten (procedure, one-time setup, job matrix); a Releases section in CONTRIBUTING.md; governance page, Python bindings page (abi3) and docs/stabilization_productionization.md updated; CHANGELOG entry.

Blockers listed in #60

Verification

  • pytest python/tests/test_release_info.py: 14 passed locally; ruff check/ruff format --check clean; actionlint clean.
  • release_info.py metadata → name=pyopenquant version=0.1.0 rust_toolchain=1.98.1; notes 0.1.0 --allow-unreleased returns the Unreleased section.
  • PR run of Release (run 36802886142): all five wheels built as cp311-abi3 (manylinux_2_28_x86_64, manylinux_2_28_aarch64, macOS x86_64, macOS arm64, win_amd64), and each smoke job passed on Python 3.11, 3.12 and 3.13 (extension _core.abi3.so / _core.pyd, Quickstart numbers identical to the docs page: portfolio_sharpe 25.303744, net_total_return -0.001511). sdist built, a wheel built from it alone passed the same smoke test. cargo publish -p openquant --dry-run packaged 120 files, verified, and aborted the upload. Publish jobs skipped.
  • workflow_dispatch with dry_run=true on this branch (run 36802888497): every job passed, including release-check and the long SADF test; publish jobs skipped.
  • The first PR run caught a Windows smoke failure (uv wants the venv directory, not Scripts/python, on Windows); fixed in the second commit.
  • CI's python jobs now build the extension as abi3 too (maturin reads the root pyproject.toml); tests and stubtest pass on 3.11 and 3.13.
  • Nothing was tagged or published.

One-time maintainer setup (checklist)

  • Decide the PyPI distribution name (Choose a distribution name and make package metadata and README accurate #58) and set [project] name in pyproject.toml (one line).
  • pypi.org → Your account → Publishing → Add a pending publisher (GitHub): project = that name, owner Open-Quant, repo openquant, workflow release.yml, environment pypi.
  • GitHub → Settings → Environments → create pypi; recommended: deployment tags limited to v* and a required reviewer.
  • crates.io → API Tokens → new token with publish-new + publish-update, scoped to crate openquant; save as repository secret CARGO_REGISTRY_TOKEN.
  • (Recommended) a tag ruleset restricting who may create v* tags.
  • Actions → Release → Run workflow on main with dry_run checked; all jobs green.

Cutting v0.1.0 once #58 is decided

  1. PR: set the name in pyproject.toml (versions already 0.1.0 in pyproject.toml, crates/openquant/Cargo.toml, crates/pyopenquant/Cargo.toml); run uv lock.
  2. Same PR: in CHANGELOG.md rename ## Unreleased to ## 0.1.0 - YYYY-MM-DD, add an empty ## Unreleased above it, update the intro paragraph ("nothing is published"); run python3 scripts/docs/generate_site_pages.py --write.
  3. Check: python3 scripts/release/release_info.py metadata --tag v0.1.0 and python3 scripts/release/release_info.py notes 0.1.0.
  4. Merge after CI and the PR's Release run pass. Optionally dispatch a dry run on main.
  5. git checkout main && git pull && git tag -a v0.1.0 -m "OpenQuant 0.1.0" && git push origin v0.1.0
  6. Approve the pypi deployment if a reviewer is required; the run publishes to PyPI and crates.io and creates the GitHub Release.
  7. Afterwards: update install instructions (README, Quickstart, Python bindings page) to pip install <name>.

🤖 Generated with Claude Code

Sean-Koval and others added 2 commits September 30, 2026 21:38
…hing (#60)

Replaces the Release Readiness workflow with a tag-driven release: abi3 wheels
for Linux x86_64/aarch64, macOS x86_64/arm64 and Windows x86_64 plus an sdist,
each installed into clean venvs on Python 3.11-3.13 and run through the
Quickstart; PyPI trusted publishing, cargo publish and a GitHub Release with
notes extracted from CHANGELOG.md, on a v* tag only. workflow_dispatch is a dry
run by default, and pull requests touching packaging files run the build half.

The PyPI distribution name is read from pyproject.toml and appears nowhere else.

Refs #60

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alls

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Sean-Koval
Sean-Koval merged commit 898412c into main Oct 1, 2026
49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant