From 6b0b2e651ec7ede755987ba3d9a8e38ed5202c28 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 31 May 2026 17:52:15 +1000 Subject: [PATCH 1/3] plan(publish): harden publish workflow and align release docs (WP1-WP3) --- .github/workflows/publish.yml | 27 +++++++++++++++++++++++++++ docs/development/release-process.md | 26 ++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 149cfc0..e0124e8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 @@ -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: diff --git a/docs/development/release-process.md b/docs/development/release-process.md index 44cfb72..ce3e0b2 100644 --- a/docs/development/release-process.md +++ b/docs/development/release-process.md @@ -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` 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 @@ -62,7 +63,7 @@ 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/ @@ -70,6 +71,23 @@ 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`. +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. From 3c67ca7512eee0c82d78702f804c777072fba247 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 31 May 2026 18:02:27 +1000 Subject: [PATCH 2/3] plan(publish): refresh release gate evidence and restore benchmark fixtures (WP4-WP6) --- docs/development/release-gate-1.0.0.md | 15 ++++- test_data/benchmarks/nrtd/NOTICE.md | 8 +++ .../hsa_000_adler_kernel_reference.parquet | Bin 0 -> 6966 bytes .../hsa_000_cholette_kernel_reference.parquet | Bin 0 -> 7021 bytes ...sa_000_dispersion_kernel_reference.parquet | Bin 0 -> 7058 bytes ..._000_laminar_flow_kernel_reference.parquet | Bin 0 -> 7094 bytes .../nrtd/hsa_000_laminar_flow_signals.parquet | Bin 0 -> 11950 bytes test_data/benchmarks/nrtd/manifest.json | 52 ++++++++++++++++++ 8 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 test_data/benchmarks/nrtd/NOTICE.md create mode 100644 test_data/benchmarks/nrtd/hsa_000_adler_kernel_reference.parquet create mode 100644 test_data/benchmarks/nrtd/hsa_000_cholette_kernel_reference.parquet create mode 100644 test_data/benchmarks/nrtd/hsa_000_dispersion_kernel_reference.parquet create mode 100644 test_data/benchmarks/nrtd/hsa_000_laminar_flow_kernel_reference.parquet create mode 100644 test_data/benchmarks/nrtd/hsa_000_laminar_flow_signals.parquet create mode 100644 test_data/benchmarks/nrtd/manifest.json diff --git a/docs/development/release-gate-1.0.0.md b/docs/development/release-gate-1.0.0.md index 42c6416..dabe10f 100644 --- a/docs/development/release-gate-1.0.0.md +++ b/docs/development/release-gate-1.0.0.md @@ -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 @@ -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: diff --git a/test_data/benchmarks/nrtd/NOTICE.md b/test_data/benchmarks/nrtd/NOTICE.md new file mode 100644 index 0000000..d4995c5 --- /dev/null +++ b/test_data/benchmarks/nrtd/NOTICE.md @@ -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 diff --git a/test_data/benchmarks/nrtd/hsa_000_adler_kernel_reference.parquet b/test_data/benchmarks/nrtd/hsa_000_adler_kernel_reference.parquet new file mode 100644 index 0000000000000000000000000000000000000000..f7f7e0e930caa9c7d4edfcfe20f9baaa8e6f4613 GIT binary patch literal 6966 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 literal 0 HcmV?d00001 diff --git a/test_data/benchmarks/nrtd/hsa_000_cholette_kernel_reference.parquet b/test_data/benchmarks/nrtd/hsa_000_cholette_kernel_reference.parquet new file mode 100644 index 0000000000000000000000000000000000000000..03441a445b03b96ffc3d1654b780e54ee3718663 GIT binary patch literal 7021 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 literal 0 HcmV?d00001 diff --git a/test_data/benchmarks/nrtd/manifest.json b/test_data/benchmarks/nrtd/manifest.json new file mode 100644 index 0000000..f572076 --- /dev/null +++ b/test_data/benchmarks/nrtd/manifest.json @@ -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" +} From 86118dbb4eefa917e6fe6d9b00666ec53068c537 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 31 May 2026 18:05:30 +1000 Subject: [PATCH 3/3] test(release-gate): verify benchmark fixture hashes against manifest --- tests/test_release_gate_benchmark_harness.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_release_gate_benchmark_harness.py b/tests/test_release_gate_benchmark_harness.py index 726251b..56ed3cc 100644 --- a/tests/test_release_gate_benchmark_harness.py +++ b/tests/test_release_gate_benchmark_harness.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json from pathlib import Path @@ -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}"