-
Notifications
You must be signed in to change notification settings - Fork 0
ReleaseProcess
Maintainer-facing reference for cutting a convert-python-sdk release. The release pipeline is fully tag-driven: pushing a v<X.Y.Z> tag to main triggers the GitHub Actions workflow that re-runs the full CI gate, compiles the changelog, builds the distributions, publishes to PyPI via OIDC Trusted Publishing, and creates a GitHub Release. No long-lived PyPI tokens are stored anywhere.
Distribution name on PyPI: convert-python-sdk
Import package: convert_sdk
Version single-sourced from: src/convert_sdk/version.py
- Land all feature work on
main— each PR must carry achanges/changelog fragment. - Bump
__version__insrc/convert_sdk/version.py. - Validate all gates locally:
uv run python scripts/verify_release.py --version X.Y.Z. - Open the version-bump PR, get CI green, and merge to
main. - Tag the merge commit
vX.Y.Zand push the tag. - The release workflow publishes to PyPI (OIDC) and creates a GitHub Release.
.github/workflows/ci.yml runs on every PR and every push to main. The release workflow reuses ci.yml verbatim as the release gate — all jobs must pass before anything is published.
| Gate | Tool | Threshold / rule |
|---|---|---|
| Lint | Ruff (E/W/F/B/SIM/RUF, line-length 100) |
Any finding on src/, tests/, scripts/ blocks merge |
| Type-check | mypy --strict on src/convert_sdk
|
Any error blocks merge |
| Tests | pytest, 15-cell matrix (Python 3.9–3.13 × ubuntu / macos / windows) | Any failing cell blocks merge |
| Coverage (project) | pytest-cov |
--cov-fail-under=85 — fails, not warns |
| Coverage (evaluation) | coverage report |
evaluation/ modules ≥ 95% — fails, not warns |
| Parity | pytest tests/parity/
|
100% pass — release-blocking (see below) |
| Dependency bounds | uv lower + upper | Both edges must pass (see below) |
| Changelog fragment | towncrier check (PRs only) |
PR touching the package without a fragment fails CI |
| Build | uv build |
Wheel + sdist must build cleanly |
Note: pytest is pinned to 8.4.x in pyproject.toml because pytest 9 drops Python 3.9 support, which is the lower edge of the CI matrix.
Reproduce all gates locally in one shot:
uv sync --group dev
uv run python scripts/verify_release.py --version 0.1.0
# Skip the slow build step while iterating:
uv run python scripts/verify_release.py --version 0.1.0 --skip-buildverify_release.py exits non-zero if any gate fails and prints a per-gate PASS/FAIL summary. It is the maintainer's local mirror of CI.
Coverage must fail, never warn. The project floor is 85% across src/convert_sdk/. The evaluation core (evaluation/) carries a stricter 95% floor because it contains the cross-SDK-critical bucketing, rule matching, and feature resolution engine. Both floors are enforced independently — passing the project floor while failing the evaluation floor still blocks the release.
Do not add # pragma: no cover to evaluation or parity code without an explicit, reviewed justification.
Parity is release-blocking. tests/parity/ runs the Python SDK's real evaluation surfaces against committed JavaScript-reference golden vectors (Story 3.5 infrastructure). The suite runs JS-runtime-free against the committed tests/parity/fixtures/*.json — no Node.js required at CI time. A divergence on a parity-critical field (reason, environment, bucket_value, variation_key, the hashed visitor reference) is a release blocker, not an advisory.
Parity fixtures are checked in so CI does not require a Node.js runtime. Regeneration is a maintainer action when the JavaScript SDK's bucketing or rule-matching behavior changes.
-
Ensure the sibling JavaScript SDK is checked out at
../javascript-sdk. -
Regenerate the golden vectors from the JS reference implementations under
scripts/js_reference/:uv run python scripts/generate_parity_fixtures.py
This refreshes
tests/parity/fixtures/*.json. -
Inspect the diff to confirm changes are intentional:
git diff tests/parity/fixtures/
-
Run the parity suite to confirm the Python SDK still matches:
uv run pytest tests/parity -x
-
If a parity-critical field diverged, fix the Python implementation (never the fixture) until the suite is green.
-
Add a
changes/fragment recording the JS SDK commit the vectors came from, and open a PR. CI re-runs the parity gate on the PR.
httpx is the SDK's only runtime dependency. The bucketing layer ships a pure-Python MurmurHash3-32 implementation — there is no mmh3 or other hashing dependency.
pyproject.toml declares a compatible-release range (httpx>=0.28,<1.0). Exact lower-bound pins live only in ci/lower-bounds-overrides.txt. Never put exact pins in pyproject.toml.
The bounds-check CI job verifies both edges:
-
Lower bound — installs
httpx==0.28.0(from the override file) on Python 3.9 and runs the unit + integration suite. This is where age-related breakage surfaces first. - Upper bound — resolves the newest compatible versions on Python 3.13 and runs the same suite.
Widening the bounds is a deliberate maintainer action, not an opportunistic one. When a new httpx version needs support: confirm the upstream changelog for breaking changes, update the range in pyproject.toml and the pin in ci/lower-bounds-overrides.txt, add a changes/ fragment, and let the bounds-check job validate both edges.
The changelog is compiled by towncrier from news fragments under changes/. Never hand-edit CHANGELOG.md.
Every PR with user-visible impact must add a fragment. This project uses PR-number-less orphan fragments named +story-{N}-{slug}.{type}.md (e.g. +story-5-2-auto-config-refresh.feature.md). Fragment types:
| Type | Use for |
|---|---|
feature |
New user-facing capability |
bugfix |
Bug fix |
breaking |
Breaking change |
deprecation |
Deprecation notice |
internal |
CI, refactoring, dependency bumps, test-only changes |
Internal-only PRs (refactors, CI changes, dependency bumps without behavior changes) should still add a changes/+…internal.md fragment to keep the missing-fragment CI gate green.
Fragments are compiled into CHANGELOG.md only at release time by the release workflow — never on a feature branch, as that causes merge conflicts.
Preview the unreleased changelog without consuming fragments:
uv run towncrier build --draft --version X.Y.Z1. Bump the version.
Edit src/convert_sdk/version.py:
__version__ = "0.2.0"This is the single source of truth. pyproject.toml declares dynamic = ["version"] and reads from this file via hatchling.
2. Validate locally.
uv sync --group dev
uv run python scripts/verify_release.py --version 0.2.0All gates must pass before opening a PR.
3. Open the version-bump PR, get CI green, and merge to main.
4. Tag and push.
git checkout main
git pull
git tag v0.2.0
git push origin v0.2.0The v* tag push is the only trigger for the release workflow.
5. The release workflow (.github/workflows/release.yml) then:
- reuses
ci.ymlas the full gate (lint, type-check, 15-cell matrix, bounds-check, parity, build), - verifies the tag matches
__version__inversion.py, - compiles the changelog with
towncrier build --yes --version X.Y.Z, - extracts the current-version section with
scripts/extract_release_notes.py, - builds the wheel + sdist with
uv build, - publishes to PyPI via OIDC Trusted Publishing,
- creates a GitHub Release with the compiled changelog and attached distributions.
6. Verify the artifact on PyPI and the GitHub Releases page.
Tags matching vX.Y.Z(a|b|rc|dev|alpha|beta)N (e.g. v0.2.0rc1) publish to PyPI and are marked as prereleases on the GitHub Release. Use these to validate the full publish flow before a stable release.
PyPI publishing uses OIDC Trusted Publishing. There are no long-lived PyPI tokens in repository secrets. This is a one-time manual action performed on pypi.org before the first release (or when setting up a new project owner).
-
Sign in to https://pypi.org as an owner of the
convert-python-sdkproject (or configure a pending publisher for a brand-new project). -
Go to Settings → Publishing → Add a new publisher (GitHub Actions).
-
Enter these values exactly:
Field Value Owner convertcomRepository python-sdkWorkflow filename release.ymlEnvironment name pypi -
Save. The
publish-pypijob'senvironment: pypiandid-token: writepermission satisfy the OIDC exchange on the nextv*tag.
If the publisher entry is misconfigured, the publish step fails with an OIDC 403 error — re-check all four fields above against the live repository values.
| Symptom | Likely cause | Fix |
|---|---|---|
tag (X) does not match package version (Y) |
Forgot to bump version.py
|
Bump __version__, delete the incorrect tag, re-tag |
| OIDC 403 on publish | Trusted Publisher entry misconfigured | Verify owner / repo / workflow / environment on pypi.org |
towncrier check fails on PR |
Missing changes/ fragment |
Add a +…{type}.md fragment |
| Project coverage < 85% | New code without tests | Add tests; never lower the floor |
evaluation/ coverage < 95% |
New evaluation code without tests | Add tests to tests/ covering the new paths |
| Parity gate fails | JS contract drift on a critical field | Fix the Python implementation, never the fixture; see parity workflow above |
| bounds-check (lower) fails | Code relies on an httpx feature not in 0.28.0 |
Fix the code or deliberately widen the lower bound with a fragment |
Copyrights © 2025 All Rights Reserved by Convert Insights, Inc.
Getting Started
Python SDK
- Quickstart
- Installation
- Initialization
- Configuration
- Code Examples
- Type Hints
- Diagnostics
- Extending
- Testing
- Async & Frameworks
Migration
Core Concepts
- Experiences & Variations
- Feature Flags
- Bucketing Algorithm
- Rule Evaluation
- Segments
- Data Management
- Event System
- API Communication
How-To Guides
- Running Experiences
- Running Features
- Tracking Conversions
- Visitor Context
- Persistent DataStore
- Troubleshooting
Edge & Integrations
Maintainers