Releasing¶
Releases go to PyPI as the ecg-records distribution. A released version can
never be replaced or re-uploaded: a defect is fixed forward in a new version.
Every step below exists to catch problems before that point.
1. Prepare the version¶
- Choose the version with Semantic Versioning. A schema minor needs at least a package minor.
- Set the version in all three places, which must agree:
versioninfeature_extraction/pyproject.toml,__version__infeature_extraction/ecgfeat/__init__.pyandLIBRARY_VERSIONinfeature_extraction/ecgfeat/pipeline/record_builder.py(written into every record's provenance andrecord_id). - Move the
## [Unreleased]entries infeature_extraction/CHANGELOG.mdinto a## [X.Y.Z] - YYYY-MM-DDsection.tools/check_changelog.pyrequires it. - If published values changed on purpose, freeze a new golden baseline (Golden corpus) and point CI at it.
1b. The engine distribution¶
Since the engine split there are two PyPI packages: the pure-Python
ecg-records (this section's steps) and the compiled ecg-records-engine
(tools/build_engine.py). They are compatible within a minor version: the
shell's engine extra requires ecg-records-engine>=<MINIMUM_ENGINE_VERSION>,<X.(Y+1)
(ecgfeat/_version.py), the engine requires
ecg-records>=<MINIMUM_LIBRARY_VERSION>,<X.(Y+1) (ecgfeat/_engine/__init__.py,
written into the wheel by tools/build_engine.py), and each side refuses the
other at run time when it is outside that range (EngineVersionError,
codes engine_version_mismatch and engine_too_old). Raise the floors when
one side starts needing the other's newer modules, as 0.3.2 did (the staged
pipeline moved into the engine). Platforms without a wheel of the new engine
then resolve ecg-records[engine] to the previous pair; a bare
pip install -U ecg-records next to an old engine gets the
engine_too_old message naming both ways out.
- New minor version (X.Y.0): release both. Upload the engine first, then the shell (the shell's release smoke test installs the engine from PyPI).
- Shell-only patch (X.Y.Z, engine unchanged): release only
ecg-records; users keep the newest engine of the same minor version (the floors stay). - Engine patch: release only
ecg-records-engineX.Y.Z; it works with every shell from itsMINIMUM_LIBRARY_VERSIONon. -
A floor was raised (0.3.2): release both, engine first. A partial platform set is safe (pip falls back to the previous pair), but upload the remaining platforms soon: until then those users stay on the previous pair.
-
The engine is built by
tools/build_engine.py: it stagesfeature_extraction/ecgfeat/_engineplusfeature_extraction/engine/(Cythonsetup.py, the two licence texts, the PyPI README, thepyproject.toml.intemplate) intobuild/engine-srcand compiles every module with Cython.tools/check_engine_wheel.pyrejects a wheel that carries engine Python source. Locally (one platform, one interpreter):
bash
python tools/build_engine.py --check # dist/engine/ecg_records_engine-<version>-cp3XY-...whl
- CI builds the full matrix (CPython 3.10–3.14 × Linux x86_64/aarch64, macOS
x86_64/arm64, Windows x86_64) with cibuildwheel and checks that the
compiled engine reproduces the golden record byte for byte
(
tools/check_engine_parity.py). Pull requests build only Linux x86_64 / CPython 3.12 (a quick gate); pushes tomainand manual runs build the full matrix. Run Actions → engine-wheels → Run workflow with the version andpublishticked to upload the wheels to PyPI once every platform built and the parity check passed. The trusted publisher for theecg-records-engineproject must name workflowengine-wheels.yml. - On a minor version change the bump list (step 1) has a fourth place: the
engineandallextras infeature_extraction/pyproject.toml(ecg-records-engine>=X.Y.0,<X.(Y+1));tests/test_engine_distribution.pyfails until they match__version__. The engine's own requirement onecg-recordsis generated bytools/build_engine.py. - Licence keys:
tools/issue_license.py issue --licensee ... --expires ...signs a key with the private signing key, which lives outside every repository (ECG_RECORDS_SIGNING_KEY), passphrase-encrypted (tools/issue_license.py protect-key --in key.pem --out key.enc.pem; the tool then asks for the passphrase, or readsECG_RECORDS_SIGNING_KEY_PASSPHRASE). Rotating the key means replacingPUBLIC_KEY_HEXinecgfeat/_engine/_license.pyand releasing a new engine. - The source repository is private; the public repository
humansys-lab/ecg-recordscarries only the documentation, schemas, changelog and examples (docs/,schemas/,examples/). After a release, first refreshdocs/site/changelog.mdfromfeature_extraction/CHANGELOG.md, then copydocs/site→docs,schemas/,feature_extraction/CHANGELOG.mdand the updated example records there and push; itsdocsworkflow deploys the site.
2. Pass the gates¶
On a clean checkout of the release commit:
python -m pytest -q tests
lint-imports --config feature_extraction/.importlinter
python tools/check_validation_registry.py
python tools/check_changelog.py
python tools/check_performance.py reference --repeats 5
python tools/build_docs.py --check && python tools/check_doc_examples.py && mkdocs build --strict
python snapshot_regression.py --data-root /data check --tier full \
--baseline-id ecg-records-<baseline> --mode record-bytes # on the dataset runner
3. Build and inspect the artifacts¶
Build from a clean checkout (not a working tree with local files), so that nothing untracked can leak into the sdist:
git worktree add /tmp/ecg-release <release-commit>
python -m build --outdir dist /tmp/ecg-release/feature_extraction # shell: wheel + sdist
(cd /tmp/ecg-release && python tools/build_engine.py --check --outdir "$OLDPWD/dist") # engine wheel for this platform; CI builds the rest
python tools/check_artifacts.py dist # required files, no tests/data/secrets, LICENSE, disclaimer
twine check --strict dist/*
(cd dist && sha256sum *.whl *.tar.gz > SHA256SUMS)
Then smoke-test clean installs outside the repository, on the oldest and newest supported Python:
python3.10 -m venv /tmp/rc310 && /tmp/rc310/bin/pip install "numpy==1.26.*" "scipy==1.11.*" "matplotlib==3.7.*" "$(ls dist/*.whl)[viz]"
(cd /tmp && /tmp/rc310/bin/python "$OLDPWD/tools/release_smoke.py" --version X.Y.Z --with-viz)
release_smoke.py checks the version, the packaged schema resources, a
canonical round trip and one extraction of a synthetic 12-lead signal. With
--with-viz it also draws a plot.
4. Publish¶
Recommended: GitHub Actions trusted publishing¶
.github/workflows/release.yml builds once, inspects, smoke-tests clean
installs (with the newest engine of the same minor version from PyPI) on Python 3.10 and
3.13, and, if you ticked upload_to_pypi when starting the run, uploads the
same bytes to PyPI. Run it once without the checkbox as a dry run if in doubt.
Finally it verifies the PyPI install, tags ecg-records-vX.Y.Z and creates a
GitHub release. It uses PyPI trusted publishing (OIDC), so no API tokens are
stored.
One-time setup:
- The GitHub environment
pypiexists and accepts deployments frommainonly (required reviewers are not available for private repositories on the organisation's plan; the maintainer's decision is theupload_to_pypi/publishcheckbox when starting a run). - On PyPI, trusted publishers (owner
humansys-lab, repositoryECG-feature-extraction-, environmentpypi): projectecg-recordswith workflowrelease.yml, and projectecg-records-enginewith workflowengine-wheels.yml. TestPyPI is not used.
Then run Actions → release → Run workflow with the version.
Alternative: manual upload with twine¶
With a project-scoped API token in ~/.pypirc (section pypi, username
__token__), after the engine wheels are on PyPI:
python3 -m venv /tmp/rc && /tmp/rc/bin/pip install "$(ls dist/ecg_records-*.whl)[viz,engine]"
(cd /tmp && /tmp/rc/bin/python "$OLDPWD/tools/release_smoke.py" --version X.Y.Z --with-viz)
twine upload dist/*.whl dist/*.tar.gz # the bytes you just tested; never rebuild
Verify that PyPI serves the same hashes as dist/SHA256SUMS
(https://pypi.org/pypi/ecg-records/X.Y.Z/json), then tag the release commit:
git tag -a ecg-records-vX.Y.Z <release-commit> -m "ecg-records X.Y.Z"
git push origin ecg-records-vX.Y.Z
5. After the release¶
- Add a new
## [Unreleased]section to the changelog. - Keep the deprecation schedule: legacy names are removed no earlier than 0.3.0, after a one-release tombstone (Compatibility).
- Revoke any API token that was scoped to the whole account, and use project-scoped tokens or trusted publishing from then on.
Release history¶
| Version | Date | Notes |
|---|---|---|
| 0.1.0 | 2026-09-24 | first release; uploaded with twine after a TestPyPI rehearsal; the uploaded hashes match the artifacts built from commit 1cb2ccc; tag ecg-records-v0.1.0 |
| 0.2.0 | 2026-10-15 | schema 1.1.0, file readers, clinical plots, first validated fields; built from the research repository's commit 6826fce (same package source as this repository's tagged commit); uploaded with twine after a TestPyPI rehearsal, together with ecginterpret 0.2.0; PyPI hashes match the local artifacts; tag ecg-records-v0.2.0 |
| 0.3.0 | 2026-10-16 | engine split: ecg-records (Apache-2.0, pure Python) + compiled ecg-records-engine (PolyForm Noncommercial or commercial); from commit 45111fa; engine wheels built locally (Linux x86_64 from 45111fa; macOS arm64, Windows AMD64 and Linux aarch64 from 0b3b43b, identical engine source, version metadata set to 0.3.0); macOS x86_64 wheels still to be added; uploaded with twine; PyPI hashes match SHA256SUMS (22/22); tag ecg-records-v0.3.0 |
| 0.3.1 | 2026-10-16 | shell-only patch (engine stays 0.3.0): PyPI project links to the docs site and the public repository; documentation version fixes; uploaded with twine; tag ecg-records-v0.3.1 |
| 0.3.2 | 2026-10-19 | shell + engine: licence-notice opt-out, summary global caption, readable query results, selector messages, public PWaveConfig, plotting gallery; engine: exact medians (same bytes, ~9% faster), legacy orchestration moved into the engine; validation re-run on the 0.3.2 engine with heart rate and frontal axes promoted to indirectly_validated; per-lead fiducials round ported from the research mainline (LUDB per-lead SD roughly halved for P; per-lead t_offset benchmark validated); staged pipeline moved into the engine, so shell and engine floors rise to 0.3.2 (MINIMUM_ENGINE_VERSION / MINIMUM_LIBRARY_VERSION); Linux x86_64 engine wheels (cp310–cp314, manylinux2014) built locally without Docker (auditwheel repair) from commit 4549d72; uploaded with twine (engine first), PyPI hashes match SHA256SUMS (7/7); clean PyPI installs on 3.10 and 3.13 pass parity and smoke; macOS arm64 and Windows AMD64 engine wheels added 2026-10-20; macOS x86_64 engine wheels (cp310–cp314, macosx_10_15) added 2026-10-22, built on Apple Silicon under Rosetta from fd6ae28 (engine source identical to 4549d72), golden parity byte-identical, PyPI hashes match SHA256SUMS (5/5), clean PyPI installs on 3.10 and 3.14 pass parity and smoke; Linux aarch64 engine wheels pending (that platform resolves to 0.3.1 + 0.3.0); tag ecg-records-v0.3.2 |