Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ jobs:
with:
python-version: "3.11"

- name: Validate release tag matches project version
run: |
python - <<'PY'
import os
import re
from pathlib import Path

tag = os.environ.get("GITHUB_REF_NAME", "")
text = Path("pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'^version = "([^"]+)"$', text, re.MULTILINE)
if match is None:
raise SystemExit("Could not find project version in pyproject.toml")

version = match.group(1)
expected_tag = f"v{version}"
if tag != expected_tag:
raise SystemExit(
f"Release tag {tag!r} does not match pyproject version {version!r}; "
f"expected {expected_tag!r}"
)

print(f"Release tag {tag} matches pyproject version {version}")
PY

- name: Install build tools
run: |
python -m pip install --upgrade pip
Expand Down Expand Up @@ -89,6 +113,9 @@ jobs:
publish:
runs-on: ubuntu-latest
needs: [validate_wheel, validate_sdist]
environment:
name: pypi
url: https://pypi.org/p/rtdfeatures
permissions:
id-token: write
steps:
Expand Down
15 changes: 13 additions & 2 deletions docs/development/release-gate-1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ TBD until merge

## Date

Sun May 31 17:21:11 ChST 2026
Sun May 31 18:01:19 ChST 2026

## Environment

- Python 3.13.5
- Python 3.12.3
- rtdfeatures 1.0.0
- Install mode: editable install with dev/examples/sklearn extras

Expand All @@ -33,6 +33,17 @@ Sun May 31 17:21:11 ChST 2026
| `pytest tests/test_sklearn_adapter.py -v` | PASS — 26 passed |
| `python examples/08_sklearn_adapter.py` | PASS |

## Publish Workflow Readiness

- `publish.yml` triggers on GitHub Release publication.
- Build job validates release tag matches `pyproject.toml` version.
- Wheel and sdist are built once and uploaded as workflow artefacts.
- Wheel install is validated in a clean virtual environment.
- Sdist install is validated in a clean virtual environment.
- Final publish job uses GitHub environment `pypi`.
- Final publish job uses PyPI trusted publishing via OIDC with `id-token: write`.
- No PyPI API token or password secret is required.

## Root API

Stable V1 root exports:
Expand Down
26 changes: 22 additions & 4 deletions docs/development/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ See [API stability policy](../api/stability.md) for the full tier definitions.
## Publishing workflow

1. **Trigger** — Publishing a GitHub Release starts `.github/workflows/publish.yml`.
2. **Build and integrity check** — The workflow builds wheel and sdist once and runs `twine check`.
3. **Artifact validation** — The workflow installs the built wheel and sdist in separate clean virtual environments, validates import path/version from the installed distribution, and runs a minimal import smoke check from outside the checkout.
4. **Trusted publish** — If both validation jobs pass, the workflow publishes the validated artifacts to PyPI using trusted publishing.
2. **Release tag/version guard** — The `build` job verifies the GitHub Release tag `v<version>` matches `version` in `pyproject.toml` before running the build.
3. **Build and integrity check** — The workflow builds wheel and sdist once and runs `twine check`.
4. **Artifact validation** — The workflow installs the built wheel and sdist in separate clean virtual environments, validates import path/version from the installed distribution, and runs a minimal import smoke check from outside the checkout.
5. **Environment-gated trusted publish** — If both validation jobs pass, the final `publish` job enters GitHub environment `pypi` for approval, then publishes the validated artifacts to PyPI using OIDC trusted publishing. No PyPI token/password secret is required in normal operation.

## Security Governance

Expand All @@ -62,14 +63,31 @@ Use these commands locally before creating a release:

```bash
python -m pip install --upgrade pip
pip install -e ".[dev]"
pip install -e ".[dev,examples,sklearn]"
pytest -m "not external_data" -v
ruff check src/ tests/
mypy src/ tests/
python -m build
twine check dist/*
```

## First PyPI release checklist

1. Confirm PyPI pending trusted publisher is configured:
- project: `rtdfeatures`
- owner: `MMerryweather`
- repository: `rtdfeatures`
- workflow: `publish.yml`
- environment: `pypi`
2. Confirm GitHub environment `pypi` exists and has the intended reviewer policy.
3. Confirm `pyproject.toml` version matches the intended release tag.
4. Run the full local release gate.
5. Merge release-prep PR to `main`.
6. Create annotated tag `v<version>`.
7. Publish a GitHub Release for that tag.
8. Approve the `pypi` environment deployment.
9. Verify install from PyPI in a clean virtual environment.

## Version history

See [CHANGELOG.md](../../CHANGELOG.md) for the full version history.
8 changes: 8 additions & 0 deletions test_data/benchmarks/nrtd/NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# nRTD Benchmark Fixture Notice

Source benchmark arrays were extracted from nRTD v1.0.0.

- Zenodo DOI: 10.5281/zenodo.15609214
- Zenodo record URL: https://zenodo.org/records/15609214
- Source archive: ChemRxnEngLab/nRTD-v1.0.0.zip
- License: MIT
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
52 changes: 52 additions & 0 deletions test_data/benchmarks/nrtd/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"assumption_events": [],
"conversion_assumptions": {
"benchmark_kernel_sum_tolerance": 1e-09,
"benchmark_regular_grid_tolerance": 1e-09,
"direct_zip_reads": true,
"kernel_weights_normalized_in_derived_columns_only": true
},
"extraction_script": "rtdfeatures.benchmarks.extract_nrtd_benchmarks",
"generated_files": [
{
"case": "adler",
"kind": "kernel_reference",
"output_path": "test_data/benchmarks/nrtd/hsa_000_adler_kernel_reference.parquet",
"sha256": "acf1b92ec9a486bcdeaa461b64014a92b879ad35af1ef6243197da93067001ac",
"source_archive_paths": []
},
{
"case": "cholette",
"kind": "kernel_reference",
"output_path": "test_data/benchmarks/nrtd/hsa_000_cholette_kernel_reference.parquet",
"sha256": "94052f1fbaa3af45a2a1cc8bd00f5b8955b417e942067bf5a07299c25e48a7b6",
"source_archive_paths": []
},
{
"case": "dispersion",
"kind": "kernel_reference",
"output_path": "test_data/benchmarks/nrtd/hsa_000_dispersion_kernel_reference.parquet",
"sha256": "209d7252515655ce065c450bf692e198a2135e8ae89e1c80b3661a3eb1d37885",
"source_archive_paths": []
},
{
"case": "laminar_flow",
"kind": "kernel_reference",
"output_path": "test_data/benchmarks/nrtd/hsa_000_laminar_flow_kernel_reference.parquet",
"sha256": "8117d9405fab71122d73f2402d85a12406405bea32f7bad119c791a646c6ed4b",
"source_archive_paths": []
},
{
"case": "laminar_flow",
"kind": "signals",
"output_path": "test_data/benchmarks/nrtd/hsa_000_laminar_flow_signals.parquet",
"sha256": "0f9cfd6544fccf13b99ad22adbae35a8a7dab2c3c83632dc129b4eae6c2a93aa",
"source_archive_paths": []
}
],
"license": "MIT",
"skipped_selected_source_files": [],
"source_archive": "ChemRxnEngLab/nRTD-v1.0.0.zip",
"zenodo_doi": "10.5281/zenodo.15609214",
"zenodo_record_url": "https://zenodo.org/records/15609214"
}
24 changes: 24 additions & 0 deletions tests/test_release_gate_benchmark_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import hashlib
import json
from pathlib import Path

Expand Down Expand Up @@ -63,3 +64,26 @@ def test_required_tracked_fixture_files_exist_and_manifest_outputs_are_present()
)
absolute = REPO_ROOT / output_path
assert absolute.exists(), f"manifest output_path missing from repository: {absolute}"


def test_manifest_generated_files_sha256_match_tracked_fixture_contents() -> None:
manifest = json.loads((BENCHMARK_DIR / "manifest.json").read_text(encoding="utf-8"))
generated_files = manifest.get("generated_files", [])
assert generated_files, "manifest.generated_files must not be empty"

for entry in generated_files:
output_path = entry.get("output_path")
expected_sha256 = entry.get("sha256")

assert isinstance(output_path, str) and output_path, (
"manifest generated_files.output_path missing"
)
assert isinstance(expected_sha256, str) and expected_sha256, (
"manifest generated_files.sha256 missing"
)

absolute = REPO_ROOT / output_path
assert absolute.exists(), f"manifest output_path missing from repository: {absolute}"

digest = hashlib.sha256(absolute.read_bytes()).hexdigest()
assert digest == expected_sha256, f"sha256 mismatch for {output_path}"
Loading