From a2e8ce2fc53fe530735b7d8a0f6907a2c51e9ab4 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:54:33 +0800 Subject: [PATCH 01/19] release: bump package metadata to 0.2.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7cd9b666..cf24a9812 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "statgpu" -version = "0.2.2" +version = "0.2.3" description = "GPU-accelerated statistical methods with sklearn-compatible API" readme = "README.md" requires-python = ">=3.9" From 3a9665ce177c648c6fa9106b19b5f2995d2e4a56 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:55:03 +0800 Subject: [PATCH 02/19] release: expose version 0.2.3 --- statgpu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/statgpu/__init__.py b/statgpu/__init__.py index 502362ec9..584b81dbe 100644 --- a/statgpu/__init__.py +++ b/statgpu/__init__.py @@ -4,7 +4,7 @@ A sklearn-compatible library for statistical computing with GPU support. """ -__version__ = "0.2.2" +__version__ = "0.2.3" from ._config import get_device, set_device, Device from ._base import BaseEstimator From 17d4505837a57fe5ccb80d2e36fb0ae2d332f693 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:55:54 +0800 Subject: [PATCH 03/19] docs: prepare 0.2.3 release notes --- CHANGELOG.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0a45ddb..0131a4234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,29 @@ # Changelog -All notable changes to statgpu are documented here, organized by date and PR. +All notable changes to statgpu are documented here, organized by release and date. -## 2026-08-04 +## 0.2.3 — 2026-08-04 -### PR #80 — Exact-source CV review-fix follow-up -- Bound the canonical physical-GPU suites to the files actually imported from the audited checkout, including runtime module paths and SHA-256 hashes. -- Converted requested CoxPHCV two-stage/successive-halving execution into one explicit exhaustive full-precision pass on NumPy, CuPy, and Torch, eliminating the repeated CuPy full-grid fit. -- Made one-shot `CoxPHCV.cv_splits` iterators reusable across repeated fit, scikit-learn clone, parameter reconstruction, and pickle without rewriting the public constructor attribute during fit. -- Published the unchanged exact-head `a726937a39eb0ed5a370dd03362884b63a9e9818` physical artifact as a durable Gist: 134/134 checks passed, all return codes were zero, every gate-failure array was empty, and the artifact SHA-256 is `e01ad0bfec238d06167caeef9955e92b6cf84eea4ccc69a3056eb794ded6eccb`. -- Bumped the final promotion report's machine schema to 3, synchronized primary CoxPH documentation and review status, and returned `.markdown` changelog archives to maintained documentation checks. These follow-up commits create a new head, so final exact-head physical promotion must be rerun before approval. +### Added +- Completed CoxPH Phase 1 with Breslow, Efron, and Exact ties; delayed-entry and `(start, stop]` counting-process data; shared-coefficient stratification; subject identifiers; and `Surv(start, stop, event)` formula input. +- Added shared NumPy, CuPy, and Torch-CUDA risk-set primitives for Cox objectives, gradients, information matrices, and baseline estimation, including backend-native dynamic programming for Exact ties. +- Extended `CoxPHCV` held-out partial likelihood to all supported tie methods, delayed entry, start-stop rows, strata, and subject-grouped folds. + +### Changed +- Hardened Cox inference, numerical stability, formula NA alignment, singular-information handling, CV cache identity, fold eligibility, selected-penalty refitting, and failed-fit state resets. +- Hardened L1, L2, Elastic Net, SCAD, and MCP penalized Cox estimation; removed the unidentified intercept; corrected Cox-specific warm starts; and made Torch Efron value, gradient, and Hessian paths native. +- Standardized public Group Lasso and Adaptive Group Lasso behavior through the generic loss-gradient and exact group-proximal path across supported backends. +- Made requested CoxPHCV two-stage and successive-halving controls execute one explicit exhaustive full-precision candidate pass, avoiding repeated complete-grid fitting while preserving deterministic selection semantics. +- Made one-shot `CoxPHCV.cv_splits` iterators reusable across repeated fit, scikit-learn clone, parameter reconstruction, and pickle. + +### Validation +- Hosted workflow #960 passed on the final reviewed head `f05a44ad363b46612e956e137e2f00d040765acb`: documentation, static, full CPU, and Python 3.9–3.12 regression jobs all passed; the complete CPU suite reported 1881 passed and 662 skipped. +- The final exact-head physical-GPU promotion artifact is published at https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4. It records schema 3, 134/134 passing checks, zero child and nested return codes, empty gate-failure arrays, clean source state before and after execution, and SHA-256 `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`. +- Added release-package validation that checks version consistency, builds the pure-Python wheel and sdist, runs `twine check`, validates artifact contents, and smoke-installs both distributions in clean environments. + +### Packaging +- Bumped the package version to `0.2.3` in `pyproject.toml` and `statgpu/__init__.py`. +- The official wheel remains a universal `py3-none-any` artifact built with `STATGPU_NO_EXT=1`; optional Cython sources remain available in the sdist. ## Earlier history From daebb337464ada2c867cd674ef12c6415b63278c Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:56:16 +0800 Subject: [PATCH 04/19] docs: publish English 0.2.3 changelog --- docs/en/changelog.md | 74 +++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/docs/en/changelog.md b/docs/en/changelog.md index cc5424eb3..8a3288191 100644 --- a/docs/en/changelog.md +++ b/docs/en/changelog.md @@ -5,35 +5,51 @@ > This page: Changelog
> Switch: [Chinese](../cn/changelog.md) -## 2026-08 - -### Fixed (2026-08-04) — PR #80 exact-source CV review follow-up - -- Canonical physical-GPU suites now prepend the audited Git checkout to - `PYTHONPATH`, disable the user site, verify that actual imported module paths - remain inside that checkout, and record SHA-256 hashes for those imported - files. Child and nested runners inherit the same controlled environment. -- Requested CoxPHCV two-stage and successive-halving controls now produce one - explicit exhaustive full-precision candidate pass on NumPy, CuPy, and Torch. - Public diagnostics report `staged_safety_strategy="single_pass_exhaustive"`; - no candidate is screened out and CuPy no longer repeats the complete grid. -- One-shot `CoxPHCV.cv_splits` iterators are materialized privately once and - reused for repeated fits, scikit-learn clone, legacy parameter reconstruction, - and pickle. Fit retains the original public constructor object. -- Hosted workflow #946 passed on exact head - `a726937a39eb0ed5a370dd03362884b63a9e9818`: the full CPU suite reported - 1879 passed and 662 skipped, while static, documentation, and Python 3.9–3.12 - regression jobs all passed. -- The unchanged physical result for that head is now durably published as - [the final promotion artifact](https://gist.github.com/TheHiddenObserver/ebbb7f2401f45b124069a30d3510c139). - It records 134/134 passing checks, zero return codes, empty gate-failure arrays, - and SHA-256 - `e01ad0bfec238d06167caeef9955e92b6cf84eea4ccc69a3056eb794ded6eccb`. -- This follow-up makes the final aggregation format truly machine schema 3, - synchronizes the primary CoxPH model pages, and brings `.markdown` archives - back under maintained documentation checks. Because these commits create a - new head, the final exact-head physical suite must be rerun before approval; - the published Gist remains valid evidence for `a726937...` only. +## 0.2.3 — 2026-08-04 + +### Survival analysis + +- Completed CoxPH Phase 1 with Breslow, Efron, and Exact ties; delayed-entry + and `(start, stop]` counting-process data; shared-coefficient stratification; + subject identifiers; and `Surv(start, stop, event)` formula input. +- Added shared NumPy, CuPy, and Torch-CUDA risk-set primitives for objectives, + gradients, information matrices, and baseline estimation. Exact tied-event + partitions use backend-native dynamic programming. +- Extended `CoxPHCV` held-out partial likelihood to all supported tie methods, + delayed entry, start-stop rows, strata, and subject-grouped folds. +- Hardened Cox inference, centered risk-set numerics, log-domain baseline + prediction, formula NA alignment, singular-information handling, CV cache + identity, fold eligibility, selected-penalty refitting, and failed-fit state + resets. +- Hardened L1, L2, Elastic Net, SCAD, and MCP penalized Cox estimation; removed + the unidentified intercept; corrected Cox-specific warm starts; and made the + Torch Efron value, gradient, and Hessian paths native. + +### Cross-validation and grouped penalties + +- Requested CoxPHCV two-stage and successive-halving controls now execute one + explicit exhaustive full-precision candidate pass, preserving deterministic + selection while avoiding repeated complete-grid fitting. +- One-shot `CoxPHCV.cv_splits` iterators are reusable across repeated fit, + scikit-learn clone, parameter reconstruction, and pickle. +- Public Group Lasso and Adaptive Group Lasso use the generic loss-gradient and + exact group-proximal path consistently across supported backends. + +### Validation and packaging + +- Hosted workflow #960 passed on final reviewed head + `f05a44ad363b46612e956e137e2f00d040765acb`: documentation, static, full CPU, + and Python 3.9–3.12 regression jobs all passed; the complete CPU suite reported + 1881 passed and 662 skipped. +- The final exact-head physical-GPU promotion artifact is published as + [schema-3 evidence](https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4). + It records 134/134 passing checks, zero child and nested return codes, empty + gate-failure arrays, clean source state before and after execution, and SHA-256 + `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`. +- The package version is now `0.2.3`. Release-package validation checks version + consistency, builds the pure-Python wheel and sdist, runs `twine check`, + validates artifact contents, and smoke-installs both distributions in clean + environments. ## Earlier history From f8d25c1b16eeb99eee883f0177e6ab207ff08376 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:56:40 +0800 Subject: [PATCH 05/19] docs: publish Chinese 0.2.3 changelog --- docs/cn/changelog.md | 67 +++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/docs/cn/changelog.md b/docs/cn/changelog.md index 1396b902b..ede007d6b 100644 --- a/docs/cn/changelog.md +++ b/docs/cn/changelog.md @@ -5,31 +5,48 @@ > 页面定位:变更记录
> 切换:[English](../en/changelog.md) -## 2026-08 - -### 修复(2026-08-04)— PR #80 精确源码 CV 复审后续 - -- 规范物理 GPU suite 现在会把受审计的 Git checkout 放在 `PYTHONPATH` - 首位、禁用 user site,核验实际导入模块的路径均位于该 checkout 内,并记录这些 - 实际导入文件的 SHA-256;child 与 nested runner 继承同一受控环境。 -- 请求 CoxPHCV two-stage 或 successive-halving 后,NumPy、CuPy 与 Torch 现在都只 - 执行一次显式 exhaustive full-precision candidate pass。公开诊断记录 - `staged_safety_strategy="single_pass_exhaustive"`;不筛除任何 candidate,CuPy 也不再 - 重复完整 grid。 -- 一次性 `CoxPHCV.cv_splits` iterator 会私下 materialize 一次,并在重复 fit、 - scikit-learn clone、旧版参数重建与 pickle 中复用;fit 期间公开构造参数对象保持不变。 -- Hosted workflow #946 已在精确 head - `a726937a39eb0ed5a370dd03362884b63a9e9818` 上通过:完整 CPU suite 为 - 1879 passed、662 skipped,static、文档及 Python 3.9–3.12 regression job 全部通过。 -- 该 head 的原始物理结果现已持久发布为 - [最终 promotion artifact](https://gist.github.com/TheHiddenObserver/ebbb7f2401f45b124069a30d3510c139)。 - Artifact 记录 134/134 项检查通过、所有 return code 为 0、所有 gate-failure 数组为空, - SHA-256 为 - `e01ad0bfec238d06167caeef9955e92b6cf84eea4ccc69a3056eb794ded6eccb`。 -- 本后续提交将 final aggregation format 正式升级为 machine schema 3,同步 CoxPH - 主模型页,并把 `.markdown` 历史页重新纳入维护文档检查。由于这些提交产生了新的 - head,最终批准前必须对新 head 再运行一次 exact-head physical suite;上述 Gist - 仍只证明 `a726937...`。 +## 0.2.3 — 2026-08-04 + +### 生存分析 + +- 完成 CoxPH Phase 1:支持 Breslow、Efron 与 Exact ties,delayed entry、 + `(start, stop]` counting-process 数据、共享系数的分层模型、subject identifier, + 以及 `Surv(start, stop, event)` 公式输入。 +- 为 NumPy、CuPy 与 Torch-CUDA 增加共享的 Cox risk-set objective、gradient、 + information matrix 与 baseline estimation primitive;Exact tied-event partition + 使用 backend-native dynamic programming。 +- `CoxPHCV` 的 held-out partial likelihood 现支持全部 tie method、delayed entry、 + start-stop row、strata 与按 subject 分组的 fold。 +- 强化 Cox inference、centered risk-set 数值计算、log-domain baseline prediction、 + 公式 NA 对齐、奇异 information 检查、CV cache identity、fold eligibility、 + selected-penalty 全数据 refit 与失败 fit 的状态清理。 +- 强化 L1、L2、Elastic Net、SCAD 与 MCP penalized Cox estimation;移除不可识别 + intercept,修正 Cox-specific warm start,并使 Torch Efron 的 value、gradient + 与 Hessian 路径保持原生实现。 + +### 交叉验证与分组惩罚 + +- 请求 CoxPHCV two-stage 或 successive-halving 时,统一执行一次显式 exhaustive + full-precision candidate pass,在保持确定性选择语义的同时避免重复完整 grid fit。 +- 一次性 `CoxPHCV.cv_splits` iterator 可在重复 fit、scikit-learn clone、参数重建 + 与 pickle 中复用。 +- 公开 Group Lasso 与 Adaptive Group Lasso 在支持的 backend 上统一采用 generic + loss-gradient 与 exact group-proximal 路径。 + +### 验证与打包 + +- Hosted workflow #960 已在最终审查 head + `f05a44ad363b46612e956e137e2f00d040765acb` 上通过:文档、static、完整 CPU + 与 Python 3.9–3.12 regression job 均通过;完整 CPU suite 为 1881 passed、 + 662 skipped。 +- 最终 exact-head 物理 GPU promotion artifact 已作为 + [schema-3 evidence](https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4) + 持久发布。它记录 134/134 项检查通过、child 与 nested return code 均为 0、 + gate-failure 数组为空、运行前后源码状态干净,SHA-256 为 + `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`。 +- 包版本更新为 `0.2.3`。新增 release-package validation:检查版本一致性,构建 + pure-Python wheel 与 sdist,执行 `twine check`,核验 artifact 内容,并在干净 + 环境中分别 smoke-install 两种发行包。 ## 更早的历史记录 From 2aeb78468f1242a2783497b12e4226b2e1b4ac39 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:57:34 +0800 Subject: [PATCH 06/19] ci: add release package validation --- .github/workflows/release-package.yml | 183 ++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .github/workflows/release-package.yml diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml new file mode 100644 index 000000000..161154206 --- /dev/null +++ b/.github/workflows/release-package.yml @@ -0,0 +1,183 @@ +name: Release package validation + +on: + pull_request: + branches: [master] + paths: + - "pyproject.toml" + - "statgpu/__init__.py" + - "setup.py" + - "MANIFEST.in" + - "README.md" + - "CHANGELOG.md" + - "docs/en/changelog.md" + - "docs/cn/changelog.md" + - "RELEASING.md" + - ".github/workflows/publish.yml" + - ".github/workflows/release-package.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-distributions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install release tooling + run: | + python -m pip install --upgrade pip + python -m pip install build twine + + - name: Verify version declarations + run: | + python - <<'PY' + import pathlib + import re + import tomllib + + pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) + init_text = pathlib.Path("statgpu/__init__.py").read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_text, re.M) + if match is None: + raise SystemExit("statgpu/__init__.py does not declare __version__") + + project_version = pyproject["project"]["version"] + package_version = match.group(1) + if project_version != package_version: + raise SystemExit( + f"version mismatch: pyproject.toml={project_version}, " + f"statgpu/__init__.py={package_version}" + ) + print(project_version) + PY + + - name: Build wheel and source distribution + env: + STATGPU_NO_EXT: "1" + run: | + rm -rf build dist *.egg-info statgpu.egg-info + python -m build + + - name: Check distribution metadata + run: python -m twine check dist/* + + - name: Validate artifact names and contents + run: | + python - <<'PY' + import pathlib + import re + import tarfile + import tomllib + import zipfile + + root = pathlib.Path.cwd() + dist = root / "dist" + version = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"] + wheel = dist / f"statgpu-{version}-py3-none-any.whl" + sdist = dist / f"statgpu-{version}.tar.gz" + + if not wheel.is_file(): + raise SystemExit(f"missing expected universal wheel: {wheel.name}") + if not sdist.is_file(): + raise SystemExit(f"missing expected source distribution: {sdist.name}") + + artifacts = sorted(path.name for path in dist.iterdir() if path.is_file()) + expected = sorted([wheel.name, sdist.name]) + if artifacts != expected: + raise SystemExit(f"unexpected dist contents: {artifacts}; expected {expected}") + + def validate_paths(names, archive): + for raw_name in names: + path = pathlib.PurePosixPath(raw_name) + if path.is_absolute() or ".." in path.parts: + raise SystemExit(f"unsafe path in {archive}: {raw_name}") + if any(part in {".git", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"} for part in path.parts): + raise SystemExit(f"cache or repository metadata in {archive}: {raw_name}") + if path.name in {".env", "credentials.json"} or path.suffix in {".pem", ".key"}: + raise SystemExit(f"credential-like file in {archive}: {raw_name}") + + with zipfile.ZipFile(wheel) as archive: + wheel_names = archive.namelist() + validate_paths(wheel_names, wheel.name) + if "statgpu/__init__.py" not in wheel_names: + raise SystemExit("wheel does not contain statgpu/__init__.py") + if any(name.endswith((".so", ".pyd", ".dll", ".dylib")) for name in wheel_names): + raise SystemExit("universal wheel unexpectedly contains compiled binaries") + + with tarfile.open(sdist, "r:gz") as archive: + sdist_names = archive.getnames() + validate_paths(sdist_names, sdist.name) + if not any(name.endswith(".pyx") for name in sdist_names): + raise SystemExit("sdist does not contain optional Cython .pyx sources") + if not any(name.endswith(".pxd") for name in sdist_names): + raise SystemExit("sdist does not contain optional Cython .pxd sources") + + metadata = next(name for name in wheel_names if name.endswith(".dist-info/METADATA")) + with zipfile.ZipFile(wheel) as archive: + metadata_text = archive.read(metadata).decode("utf-8") + if not re.search(rf"^Version: {re.escape(version)}$", metadata_text, re.M): + raise SystemExit("wheel metadata version does not match pyproject.toml") + + print(f"validated {wheel.name} and {sdist.name}") + PY + + - name: Smoke-install wheel in a clean environment + run: | + WHEEL="$(realpath dist/*.whl)" + python -m venv "$RUNNER_TEMP/statgpu-wheel-test" + "$RUNNER_TEMP/statgpu-wheel-test/bin/python" -m pip install --upgrade pip + "$RUNNER_TEMP/statgpu-wheel-test/bin/python" -m pip install "$WHEEL" + cd "$RUNNER_TEMP" + "$RUNNER_TEMP/statgpu-wheel-test/bin/python" - <<'PY' + import pathlib + import tomllib + import statgpu + from statgpu.linear_model import LinearRegression + from statgpu.survival import CoxPH, CoxPHCV + + expected = tomllib.loads( + pathlib.Path("${GITHUB_WORKSPACE}/pyproject.toml").read_text(encoding="utf-8") + )["project"]["version"] + assert statgpu.__version__ == expected + assert LinearRegression is not None + assert CoxPH is not None + assert CoxPHCV is not None + print(statgpu.__version__) + PY + + - name: Smoke-install sdist in a clean environment + env: + STATGPU_NO_EXT: "1" + run: | + SDIST="$(realpath dist/*.tar.gz)" + python -m venv "$RUNNER_TEMP/statgpu-sdist-test" + "$RUNNER_TEMP/statgpu-sdist-test/bin/python" -m pip install --upgrade pip + "$RUNNER_TEMP/statgpu-sdist-test/bin/python" -m pip install "$SDIST" + cd "$RUNNER_TEMP" + "$RUNNER_TEMP/statgpu-sdist-test/bin/python" - <<'PY' + import pathlib + import tomllib + import statgpu + + expected = tomllib.loads( + pathlib.Path("${GITHUB_WORKSPACE}/pyproject.toml").read_text(encoding="utf-8") + )["project"]["version"] + assert statgpu.__version__ == expected + print(statgpu.__version__) + PY + + - name: Upload validated distributions + uses: actions/upload-artifact@v4 + with: + name: statgpu-release-distributions + path: dist/* + if-no-files-found: error + retention-days: 7 From 85df8382fe519856077efc3034e3c17ff640a061 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:59:09 +0800 Subject: [PATCH 07/19] ci: fix clean-install workspace lookup --- .github/workflows/release-package.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 161154206..f755e4a75 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -137,15 +137,15 @@ jobs: "$RUNNER_TEMP/statgpu-wheel-test/bin/python" -m pip install "$WHEEL" cd "$RUNNER_TEMP" "$RUNNER_TEMP/statgpu-wheel-test/bin/python" - <<'PY' + import os import pathlib import tomllib import statgpu from statgpu.linear_model import LinearRegression from statgpu.survival import CoxPH, CoxPHCV - expected = tomllib.loads( - pathlib.Path("${GITHUB_WORKSPACE}/pyproject.toml").read_text(encoding="utf-8") - )["project"]["version"] + pyproject = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) / "pyproject.toml" + expected = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["version"] assert statgpu.__version__ == expected assert LinearRegression is not None assert CoxPH is not None @@ -163,13 +163,13 @@ jobs: "$RUNNER_TEMP/statgpu-sdist-test/bin/python" -m pip install "$SDIST" cd "$RUNNER_TEMP" "$RUNNER_TEMP/statgpu-sdist-test/bin/python" - <<'PY' + import os import pathlib import tomllib import statgpu - expected = tomllib.loads( - pathlib.Path("${GITHUB_WORKSPACE}/pyproject.toml").read_text(encoding="utf-8") - )["project"]["version"] + pyproject = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) / "pyproject.toml" + expected = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["version"] assert statgpu.__version__ == expected print(statgpu.__version__) PY From 9d6bffcee9586515f9767612c42bca336fe6e39b Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:59:35 +0800 Subject: [PATCH 08/19] ci: harden PyPI publish validation --- .github/workflows/publish.yml | 73 +++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2cdf0f659..9e49632a6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ name: Publish to PyPI on: push: tags: - - 'v*' + - "v*" permissions: contents: read @@ -17,34 +17,75 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: "3.11" - name: Install build tools run: | python -m pip install --upgrade pip python -m pip install build twine - - name: Verify tag matches package version + - name: Verify tag and package versions run: | - TAG_VERSION=${GITHUB_REF#refs/tags/v} - PKG_VERSION=$(python -c "import re; print(re.search(r\"version\s*=\s*['\\\"]([^'\\\"]+)['\\\"]\", open('pyproject.toml').read()).group(1))") - if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then - echo "ERROR: Tag version ($TAG_VERSION) does not match package version ($PKG_VERSION)" - exit 1 - fi - - - name: Build package (pure-Python wheel + sdist) - # STATGPU_NO_EXT=1 -> no compiled extensions, so the wheel is tagged - # py3-none-any (universal). The sdist still ships .pyx/.pxd sources for - # users who want to build the optional C accelerators locally. + python - <<'PY' + import os + import pathlib + import re + import tomllib + + tag = os.environ["GITHUB_REF_NAME"] + if not tag.startswith("v"): + raise SystemExit(f"release tag must start with v: {tag}") + tag_version = tag[1:] + + pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) + project_version = pyproject["project"]["version"] + init_text = pathlib.Path("statgpu/__init__.py").read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_text, re.M) + if match is None: + raise SystemExit("statgpu/__init__.py does not declare __version__") + package_version = match.group(1) + + if len({tag_version, project_version, package_version}) != 1: + raise SystemExit( + "release version mismatch: " + f"tag={tag_version}, pyproject.toml={project_version}, " + f"statgpu/__init__.py={package_version}" + ) + print(tag_version) + PY + + - name: Build package (pure-Python wheel and sdist) env: STATGPU_NO_EXT: "1" - run: python -m build + run: | + rm -rf build dist *.egg-info statgpu.egg-info + python -m build - name: Check distributions run: | python -m twine check dist/* - ls -l dist/ + ls -lh dist/ + + - name: Smoke-install release wheel + run: | + WHEEL="$(realpath dist/*.whl)" + python -m venv "$RUNNER_TEMP/statgpu-publish-smoke" + "$RUNNER_TEMP/statgpu-publish-smoke/bin/python" -m pip install --upgrade pip + "$RUNNER_TEMP/statgpu-publish-smoke/bin/python" -m pip install "$WHEEL" + cd "$RUNNER_TEMP" + "$RUNNER_TEMP/statgpu-publish-smoke/bin/python" - <<'PY' + import os + import statgpu + from statgpu.linear_model import LinearRegression + from statgpu.survival import CoxPH, CoxPHCV + + expected = os.environ["GITHUB_REF_NAME"][1:] + assert statgpu.__version__ == expected + assert LinearRegression is not None + assert CoxPH is not None + assert CoxPHCV is not None + print(statgpu.__version__) + PY - name: Publish to PyPI env: From 41a0c039fe6af2fee325932038a1aedb473c10fd Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:00:16 +0800 Subject: [PATCH 09/19] docs: update the PyPI release procedure --- RELEASING.md | 232 +++++++++++++++++++++------------------------------ 1 file changed, 96 insertions(+), 136 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 773db49d5..7d6224a90 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,6 +1,10 @@ # Releasing statgpu to PyPI -This document is for maintainers preparing an official `statgpu` release. The repository currently publishes from GitHub Actions when a tag matching `v*` is pushed. The workflow is defined in [`.github/workflows/publish.yml`](.github/workflows/publish.yml). +This document is for maintainers preparing an official `statgpu` release. +The repository publishes from GitHub Actions when a tag matching `v*` is pushed. +The upload workflow is defined in [`.github/workflows/publish.yml`](.github/workflows/publish.yml), +and pull-request package validation is defined in +[`.github/workflows/release-package.yml`](.github/workflows/release-package.yml). ## Release model @@ -9,163 +13,107 @@ The package version is maintained in two files and must match: - `pyproject.toml`: `project.version`; - `statgpu/__init__.py`: `__version__`. -A release tag must use the same version with a leading `v`, for example: +A release tag uses the same version with a leading `v`: ```text -package version: 0.2.2 -tag: v0.2.2 +package version: 0.2.3 +tag: v0.2.3 ``` -PyPI release files are immutable. A broken upload cannot be replaced under the same version; prepare a new patch version instead. +PyPI release files are immutable. A broken upload cannot be replaced under the +same version; prepare a new patch version instead. ## 1. Prepare a focused release pull request -Start from the latest `master` after the intended feature/fix pull requests are merged. +Start from the latest `master` after the intended feature and fix pull requests +are merged. -Update both version declarations: - -```toml -# pyproject.toml -version = "0.2.2" -``` - -```python -# statgpu/__init__.py -__version__ = "0.2.2" -``` - -Update release-facing documentation: +Update both version declarations and the release-facing documentation: +- `pyproject.toml`; +- `statgpu/__init__.py`; - `CHANGELOG.md`; - `docs/en/changelog.md`; - `docs/cn/changelog.md`; -- README or model documentation when installation, compatibility, or public behavior changed. +- README or model documentation when installation, compatibility, or public + behavior changed. -Keep release-only changes separate from large implementation work. The release pull request should primarily contain version, packaging, changelog, and release-validation updates. +Keep release-only changes separate from implementation work. A release pull +request should primarily contain version, packaging, changelog, and +release-validation changes. -## 2. Validate the release candidate +## 2. Validate the release pull request -At minimum, run the full CPU suite: +The normal `Tests` workflow must pass, including the complete CPU suite, static +contracts, documentation contracts, and the Python 3.9–3.12 regression matrix. +For changes affecting CuPy, Torch, inference, device routing, or performance, +record physical-GPU acceptance on the exact release source commit. -```bash -python -m pip install -e ".[dev,validation,formula]" -python -m pytest dev/tests -q --tb=short -``` +The `Release package validation` workflow automatically: -Run focused physical-GPU acceptance for changes that affect CuPy, Torch, inference, device routing, or performance. Record the exact commit, GPU, CUDA/CuPy/Torch versions, and whether any test was skipped. +1. checks that `pyproject.toml` and `statgpu/__init__.py` declare the same version; +2. builds a pure-Python wheel and source distribution with `STATGPU_NO_EXT=1`; +3. runs `twine check`; +4. requires exactly `statgpu-X.Y.Z-py3-none-any.whl` and + `statgpu-X.Y.Z.tar.gz`; +5. rejects unsafe paths, credential-like files, cache directories, and compiled + binaries in the universal wheel; +6. confirms that the sdist contains the optional `.pyx` and `.pxd` sources; +7. installs the wheel and sdist in separate clean virtual environments and runs + import/version smoke tests; +8. uploads the validated distributions as a short-lived workflow artifact. -Confirm that both version declarations agree: +For a local rehearsal, run: ```bash -python - <<'PY' -import pathlib -import re - -pyproject = pathlib.Path("pyproject.toml").read_text(encoding="utf-8") -init_file = pathlib.Path("statgpu/__init__.py").read_text(encoding="utf-8") - -project_version = re.search(r'^version\s*=\s*["\']([^"\']+)["\']', pyproject, re.M).group(1) -package_version = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_file, re.M).group(1) -assert project_version == package_version, (project_version, package_version) -print(project_version) -PY -``` - -## 3. Build clean artifacts locally - -Remove stale packaging output first: +python -m pip install -e ".[dev,validation,formula]" +python -m pytest dev/tests -q --tb=short -```bash rm -rf build dist *.egg-info statgpu.egg-info python -m pip install --upgrade build twine -``` - -The official PyPI workflow sets `STATGPU_NO_EXT=1`. This produces a universal pure-Python wheel while retaining optional Cython sources in the sdist: - -```bash STATGPU_NO_EXT=1 python -m build python -m twine check dist/* ls -lh dist/ ``` -Expected artifacts: - -```text -statgpu-X.Y.Z-py3-none-any.whl -statgpu-X.Y.Z.tar.gz -``` - -`MANIFEST.in` includes the `.pyx` and `.pxd` files required by users who choose to build the optional CPU extensions from the sdist. - -## 4. Test the wheel and sdist in clean environments +Do not validate only from the source checkout. Install the wheel and sdist in +fresh environments, or rely on the successful release-package workflow for the +exact PR head. -Do not validate only from the source checkout. Install each artifact in a fresh environment. +## 3. Optional TestPyPI rehearsal -### Wheel - -```bash -python -m venv /tmp/statgpu-wheel-test -/tmp/statgpu-wheel-test/bin/python -m pip install --upgrade pip -/tmp/statgpu-wheel-test/bin/python -m pip install dist/statgpu-X.Y.Z-py3-none-any.whl -/tmp/statgpu-wheel-test/bin/python - <<'PY' -import statgpu -print(statgpu.__version__) -from statgpu.linear_model import LinearRegression -print(LinearRegression) -PY -``` - -### Source distribution - -```bash -python -m venv /tmp/statgpu-sdist-test -/tmp/statgpu-sdist-test/bin/python -m pip install --upgrade pip -STATGPU_NO_EXT=1 /tmp/statgpu-sdist-test/bin/python -m pip install dist/statgpu-X.Y.Z.tar.gz -/tmp/statgpu-sdist-test/bin/python - <<'PY' -import statgpu -print(statgpu.__version__) -PY -``` - -On Windows, replace `/tmp/.../bin/python` with the environment's `Scripts/python.exe`. - -For packaging changes, also inspect the artifact contents and confirm that no credentials, benchmark caches, local configuration, or unrelated result bundles are included. - -## 5. Optional TestPyPI rehearsal - -A TestPyPI upload is recommended when changing packaging metadata, package discovery, build behavior, dependencies, or release automation. +A TestPyPI rehearsal is recommended when changing packaging metadata, package +discovery, build behavior, dependencies, or release automation: ```bash python -m twine upload --repository testpypi dist/* -``` - -Install with PyPI available for dependencies: - -```bash python -m pip install \ --index-url https://test.pypi.org/simple/ \ --extra-index-url https://pypi.org/simple/ \ statgpu==X.Y.Z ``` -TestPyPI and PyPI require separate credentials/tokens. +TestPyPI and PyPI use separate credentials. -## 6. Merge the release pull request +## 4. Merge the release pull request Before merging, verify: -- version fields match; -- changelogs describe the release accurately; -- CI is green on the exact release head; -- required physical-GPU tests are recorded; -- wheel and sdist both pass `twine check` and clean-install tests; -- the target version does not already exist on PyPI. +- both version declarations match the intended release; +- all release notes are accurate and synchronized in English and Chinese; +- required GitHub Actions jobs are green on the exact release head; +- required physical-GPU evidence is recorded for the exact source commit; +- wheel and sdist validation and clean-install smoke tests pass; +- the target version does not already exist on PyPI; +- the `PYPI_TOKEN` repository secret remains valid and project-scoped. -Merge the focused release pull request into `master`. +Merge the focused release pull request into `master`. Do not add unrelated +commits after release validation; changes after validation require the release +checks to run again. -## 7. Create and push the release tag +## 5. Create and push the release tag -Update local `master` and tag the exact merge commit: +Update local `master` and tag the exact release-PR merge commit: ```bash git checkout master @@ -174,21 +122,22 @@ git tag -a vX.Y.Z -m "statgpu X.Y.Z" git push origin vX.Y.Z ``` -Pushing the tag starts the `Publish to PyPI` workflow. The current workflow: +Pushing the tag starts `Publish to PyPI`. The workflow: 1. checks out the tagged commit; -2. sets up Python 3.11; -3. installs `build` and `twine`; -4. verifies that the tag matches `pyproject.toml`; -5. builds a pure-Python wheel and sdist with `STATGPU_NO_EXT=1`; -6. runs `twine check`; -7. uploads `dist/*` to PyPI using the repository secret `PYPI_TOKEN`. +2. verifies that the tag, `pyproject.toml`, and `statgpu.__version__` agree; +3. builds a pure-Python wheel and sdist with `STATGPU_NO_EXT=1`; +4. runs `twine check`; +5. installs the wheel in a clean environment and checks its version and core + imports; +6. uploads `dist/*` to PyPI using the repository secret `PYPI_TOKEN`. -The PyPI API token should be project-scoped and stored only as a GitHub Actions secret. Never place it in source files, command history committed to the repository, issue comments, or documentation examples. +Never place the PyPI token in source files, committed command output, issues, +pull requests, or documentation examples. -## 8. Verify the published release +## 6. Verify the published release -After the workflow succeeds, verify the PyPI release in a new environment: +After the workflow succeeds, verify the release from a new environment: ```bash python -m venv /tmp/statgpu-pypi-test @@ -196,37 +145,48 @@ python -m venv /tmp/statgpu-pypi-test /tmp/statgpu-pypi-test/bin/python -m pip install --no-cache-dir statgpu==X.Y.Z /tmp/statgpu-pypi-test/bin/python - <<'PY' import statgpu +from statgpu.survival import CoxPH, CoxPHCV + print(statgpu.__version__) +print(CoxPH, CoxPHCV) PY ``` Also verify: -- the PyPI project page renders the README correctly; -- the wheel is `py3-none-any` as intended; +- the PyPI page renders the README correctly; +- the wheel is `py3-none-any`; - the sdist is present; -- dependency extras are displayed; -- the homepage and repository links are valid. - -Create a GitHub Release from the same tag and use the changelog as the basis for release notes. +- dependency extras and supported Python versions are correct; +- project, documentation, issue, and changelog links work; +- a GitHub Release is created from the same tag using the changelog as the basis + for release notes. -## 9. Failure handling +## 7. Failure handling ### Version mismatch -If the tag and package version differ, the workflow stops before uploading. Correct the version in a new commit and create a new tag. Do not move an already published tag. +The publish workflow stops before upload. Correct the version in a new commit, +merge a new release PR, and create a new tag. Do not move a published tag. -### Upload partially succeeds +### Partial upload -PyPI may accept one artifact before another fails. Because filenames and versions are immutable, inspect the project release and normally issue a new patch version rather than attempting to replace uploaded files. +PyPI may accept one artifact before another fails. Because filenames and +versions are immutable, inspect the release and normally issue a new patch +version rather than trying to replace the accepted file. ### Bad release already published -- mark the PyPI release as yanked when appropriate; +- yank the PyPI release when appropriate; - fix the problem in a new patch release; - document the incident and migration path in the changelog; -- do not delete or recreate Git history to reuse the version. +- do not rewrite Git history or reuse the released version. -## Recommended automation improvement +## Future infrastructure improvement -The current workflow uses a project-scoped API token through `PYPI_TOKEN`. PyPI Trusted Publishing is preferable for long-term maintenance because it removes the stored upload token and binds publishing to a specific GitHub repository/workflow/environment. Migrating should be handled in a dedicated release-infrastructure pull request and tested before removing the existing token path. +The current workflow uses a project-scoped API token through `PYPI_TOKEN`. +PyPI Trusted Publishing is preferable for long-term maintenance because it +removes the stored upload token and binds publishing to a specific repository, +workflow, and optional environment. Migrate in a dedicated infrastructure pull +request and verify the trusted-publisher configuration before removing the token +path. From 531fccbdc6182235de57d28d540ab21166599367 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:09:43 +0800 Subject: [PATCH 10/19] fix(ci): validate existing Cython sources --- .github/workflows/release-package.yml | 31 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index f755e4a75..ebdd06002 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -113,12 +113,28 @@ jobs: raise SystemExit("universal wheel unexpectedly contains compiled binaries") with tarfile.open(sdist, "r:gz") as archive: - sdist_names = archive.getnames() + sdist_names = set(archive.getnames()) validate_paths(sdist_names, sdist.name) - if not any(name.endswith(".pyx") for name in sdist_names): - raise SystemExit("sdist does not contain optional Cython .pyx sources") - if not any(name.endswith(".pxd") for name in sdist_names): - raise SystemExit("sdist does not contain optional Cython .pxd sources") + + cython_sources = sorted( + path.relative_to(root).as_posix() + for path in (root / "statgpu").rglob("*") + if path.is_file() and path.suffix in {".pyx", ".pxd"} + ) + if not cython_sources: + raise SystemExit("repository contains no optional Cython sources to validate") + + sdist_prefix = f"statgpu-{version}/" + missing_cython_sources = [ + path + for path in cython_sources + if f"{sdist_prefix}{path}" not in sdist_names + ] + if missing_cython_sources: + raise SystemExit( + "sdist is missing repository Cython sources: " + + ", ".join(missing_cython_sources) + ) metadata = next(name for name in wheel_names if name.endswith(".dist-info/METADATA")) with zipfile.ZipFile(wheel) as archive: @@ -126,7 +142,10 @@ jobs: if not re.search(rf"^Version: {re.escape(version)}$", metadata_text, re.M): raise SystemExit("wheel metadata version does not match pyproject.toml") - print(f"validated {wheel.name} and {sdist.name}") + print( + f"validated {wheel.name}, {sdist.name}, and " + f"{len(cython_sources)} repository Cython sources" + ) PY - name: Smoke-install wheel in a clean environment From 7fb359e8afccee36ad1978bb691658aa8b6a1c25 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:10:24 +0800 Subject: [PATCH 11/19] docs: align Cython source validation --- RELEASING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index 7d6224a90..462e49fa5 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -58,7 +58,8 @@ The `Release package validation` workflow automatically: `statgpu-X.Y.Z.tar.gz`; 5. rejects unsafe paths, credential-like files, cache directories, and compiled binaries in the universal wheel; -6. confirms that the sdist contains the optional `.pyx` and `.pxd` sources; +6. confirms that the sdist contains every `.pyx` or `.pxd` source that currently + exists in the repository; 7. installs the wheel and sdist in separate clean virtual environments and runs import/version smoke tests; 8. uploads the validated distributions as a short-lived workflow artifact. From 1c6fd9da738f4bbc17684ae5ff53ccbf3842a983 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:26:45 +0800 Subject: [PATCH 12/19] ci: validate release wheel across platforms --- .github/workflows/release-package.yml | 120 ++++++++++++++++++++------ 1 file changed, 96 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index ebdd06002..1f7e8163b 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -148,30 +148,6 @@ jobs: ) PY - - name: Smoke-install wheel in a clean environment - run: | - WHEEL="$(realpath dist/*.whl)" - python -m venv "$RUNNER_TEMP/statgpu-wheel-test" - "$RUNNER_TEMP/statgpu-wheel-test/bin/python" -m pip install --upgrade pip - "$RUNNER_TEMP/statgpu-wheel-test/bin/python" -m pip install "$WHEEL" - cd "$RUNNER_TEMP" - "$RUNNER_TEMP/statgpu-wheel-test/bin/python" - <<'PY' - import os - import pathlib - import tomllib - import statgpu - from statgpu.linear_model import LinearRegression - from statgpu.survival import CoxPH, CoxPHCV - - pyproject = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) / "pyproject.toml" - expected = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["version"] - assert statgpu.__version__ == expected - assert LinearRegression is not None - assert CoxPH is not None - assert CoxPHCV is not None - print(statgpu.__version__) - PY - - name: Smoke-install sdist in a clean environment env: STATGPU_NO_EXT: "1" @@ -200,3 +176,99 @@ jobs: path: dist/* if-no-files-found: error retention-days: 7 + + smoke-install-wheel: + name: wheel smoke (${{ matrix.os }}) + needs: validate-distributions + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Download validated distributions + uses: actions/download-artifact@v4 + with: + name: statgpu-release-distributions + path: dist + + - name: Smoke-install universal wheel + shell: python + run: | + import os + import pathlib + import subprocess + import tempfile + import tomllib + import venv + + root = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) + version = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"] + wheel = root / "dist" / f"statgpu-{version}-py3-none-any.whl" + if not wheel.is_file(): + raise SystemExit(f"missing downloaded wheel: {wheel}") + + temp_root = pathlib.Path(tempfile.mkdtemp(prefix="statgpu-wheel-smoke-")) + env_dir = temp_root / "venv" + venv.EnvBuilder(with_pip=True, clear=True).create(env_dir) + if os.name == "nt": + env_python = env_dir / "Scripts" / "python.exe" + else: + env_python = env_dir / "bin" / "python" + + subprocess.run( + [str(env_python), "-m", "pip", "install", "--upgrade", "pip"], + check=True, + ) + subprocess.run( + [str(env_python), "-m", "pip", "install", str(wheel)], + check=True, + ) + + smoke_code = r''' + import os + import numpy as np + import statgpu + from statgpu.linear_model import LinearRegression + from statgpu.survival import CoxPH, CoxPHCV + + expected = os.environ["EXPECTED_STATGPU_VERSION"] + assert statgpu.__version__ == expected + + X = np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + [0.0, 1.0], + [1.0, 1.0], + [2.0, 1.0], + [1.0, 2.0], + ], + dtype=float, + ) + y = 1.0 + 2.0 * X[:, 0] - 0.5 * X[:, 1] + model = LinearRegression(device="cpu") + model.fit(X, y) + prediction = np.asarray(model.predict(X)) + assert prediction.shape == y.shape + assert np.isfinite(prediction).all() + assert CoxPH is not None + assert CoxPHCV is not None + print(statgpu.__version__) + ''' + + smoke_env = os.environ.copy() + smoke_env["EXPECTED_STATGPU_VERSION"] = version + subprocess.run( + [str(env_python), "-c", smoke_code], + cwd=temp_root, + env=smoke_env, + check=True, + ) From fec4341987c51f2be6c16378084b9f015e351c98 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:27:34 +0800 Subject: [PATCH 13/19] docs: document cross-platform release smoke matrix --- RELEASING.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 462e49fa5..8521e0450 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -60,9 +60,16 @@ The `Release package validation` workflow automatically: binaries in the universal wheel; 6. confirms that the sdist contains every `.pyx` or `.pxd` source that currently exists in the repository; -7. installs the wheel and sdist in separate clean virtual environments and runs - import/version smoke tests; -8. uploads the validated distributions as a short-lived workflow artifact. +7. installs the sdist in a clean Ubuntu virtual environment and checks its + version; +8. uploads the validated wheel and sdist as a short-lived workflow artifact; +9. downloads that exact wheel artifact on Ubuntu, Windows, and macOS, installs it + in a fresh virtual environment, imports the public linear-model and Cox APIs, + and runs a CPU `LinearRegression` fit/predict smoke test. + +The cross-platform matrix validates portability of the published +`py3-none-any` CPU wheel. It does not claim Apple MPS support or replace the +separate physical-NVIDIA-GPU acceptance required for CUDA behavior. For a local rehearsal, run: @@ -104,7 +111,8 @@ Before merging, verify: - all release notes are accurate and synchronized in English and Chinese; - required GitHub Actions jobs are green on the exact release head; - required physical-GPU evidence is recorded for the exact source commit; -- wheel and sdist validation and clean-install smoke tests pass; +- wheel and sdist validation, the Ubuntu sdist clean-install check, and the + Ubuntu/Windows/macOS wheel smoke matrix pass; - the target version does not already exist on PyPI; - the `PYPI_TOKEN` repository secret remains valid and project-scoped. From 60e745dbf3a20bcdd3e1b8aea6c7e5bc23c2990a Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:43:34 +0800 Subject: [PATCH 14/19] docs: add complete v0.2.3 GitHub release notes --- .github/releases/v0.2.3.md | 155 +++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 .github/releases/v0.2.3.md diff --git a/.github/releases/v0.2.3.md b/.github/releases/v0.2.3.md new file mode 100644 index 000000000..cb9b9a94f --- /dev/null +++ b/.github/releases/v0.2.3.md @@ -0,0 +1,155 @@ +# statgpu 0.2.3 + +statgpu 0.2.3 is a substantial survival-analysis release. It completes the first major Cox proportional-hazards implementation phase, expands Cox model selection and penalized estimation, and hardens numerical, inference, and packaging behavior across the supported NumPy, CuPy, and PyTorch backends. + +## Highlights + +- Complete CoxPH support for **Breslow, Efron, and Exact ties**. +- Delayed entry and `(start, stop]` counting-process data. +- Shared-coefficient stratified Cox models with stratum-specific baselines. +- Subject-aware repeated-row handling and `Surv(start, stop, event)` formula input. +- Extended `CoxPHCV` scoring, fold construction, diagnostics, and final refitting. +- Hardened L1, L2, Elastic Net, SCAD, and MCP penalized Cox estimation. +- Universal `py3-none-any` wheel validated on Linux, Windows, and macOS. + +## CoxPH Phase 1 completion + +### Tie handling and risk sets + +`CoxPH` now supports all three primary tie methods: + +- `ties="breslow"`; +- `ties="efron"`; +- `ties="exact"`. + +The implementation uses shared NumPy, CuPy, and Torch-CUDA risk-set primitives for the partial-likelihood objective, gradient, information matrix, and baseline estimation. Exact tied-event partitions use backend-native dynamic programming rather than a CPU-only implementation. + +### Delayed entry, start-stop rows, and stratification + +The public Cox interface now supports: + +- delayed entry; +- `(start, stop]` counting-process rows; +- repeated rows belonging to the same subject; +- shared coefficients with stratum-specific baseline hazards; +- formula input through `Surv(start, stop, event)`. + +Formula-driven NA removal now keeps entry, cluster, strata, subject, response, and design arrays aligned. + +### Inference and baseline prediction + +For Breslow and Efron fits, model-based, HC0, HC1, and cluster covariance are supported where the requested data configuration is eligible. Exact ties currently support model-based covariance only; unsupported robust-covariance requests fail explicitly instead of silently changing behavior. + +Cox numerical stability has been strengthened through centered risk-set moments and log-domain baseline prediction. Singular information matrices are rejected rather than returning misleading zero standard errors. + +Baseline prediction requires `compute_inference=True`. The conventional Breslow baseline estimator is used after coefficient fitting, including when coefficients were fitted with Efron or Exact ties. + +## CoxPHCV completion + +`CoxPHCV` held-out partial likelihood now supports: + +- Breslow, Efron, and Exact ties; +- delayed entry and start-stop data; +- strata; +- subject-grouped folds, so repeated rows from one subject remain in one fold; +- device-native held-out scoring; +- convergence- and failure-aware candidate diagnostics; +- selected-penalty refitting on the complete dataset. + +Full-data cache identities, fold validation, cloneability, repeated fitting, pickling, and failed-refit state cleanup were hardened. + +Requested two-stage or successive-halving controls currently execute one deterministic exhaustive full-precision candidate pass. This avoids repeated complete-grid fitting without pretending that unsafe screening has occurred. + +## Penalized Cox and grouped penalties + +Penalized Cox estimation was hardened for: + +- L1; +- L2; +- Elastic Net; +- SCAD; +- MCP. + +The unidentified Cox intercept was removed, Cox-specific SCAD/MCP warm starts were corrected, and Torch Efron value, gradient, and Hessian paths remain native rather than routing through CuPy. + +`PenalizedCoxPHModel` remains an estimation-only API in this release. Passing `compute_inference=True` raises `NotImplementedError` explicitly. + +Public Group Lasso and Adaptive Group Lasso behavior now follows the generic loss-gradient and exact group-proximal implementation consistently across supported backends. + +## Reliability and API hardening + +This release also improves: + +- failed-fit and failed-refit state resets; +- singular-information handling; +- censoring- and tie-correct concordance semantics; +- device-label grouping without accidental coercion; +- independence from optional statsmodels for robust Cox covariance; +- CV cache identity and candidate eligibility; +- one-shot `cv_splits` iterator reuse across repeated fit, clone, reconstruction, and pickle. + +## Installation and platform support + +Base CPU installation: + +```bash +pip install statgpu==0.2.3 +``` + +CUDA extras: + +```bash +pip install "statgpu[gpu11]==0.2.3" +pip install "statgpu[gpu12]==0.2.3" +``` + +PyTorch backend: + +```bash +pip install "statgpu[torch]==0.2.3" +``` + +The published wheel is a pure-Python `py3-none-any` artifact. The exact release wheel is clean-installed and exercised on Ubuntu, Windows, and macOS using Python 3.11, including a CPU `LinearRegression.fit/predict` smoke test and public Cox imports. The broader regression suite covers Python 3.9–3.12 on Ubuntu. + +The cross-platform wheel validation establishes CPU-wheel portability. It does not add Apple MPS support. CUDA execution still requires a compatible NVIDIA driver/runtime and the matching CuPy or PyTorch package. + +Optional Cython CPU accelerators are not embedded in the universal wheel. Their `.pyx`/`.pxd` sources remain in the source distribution for local builds. + +## Validation + +The final reviewed implementation head for the CoxPH Phase 1 work is: + +```text +f05a44ad363b46612e956e137e2f00d040765acb +``` + +Hosted workflow #960 passed documentation, static, full CPU, and Python 3.9–3.12 regression jobs. The complete CPU suite reported **1881 passed and 662 skipped**. + +The exact-head physical-GPU promotion artifact records: + +- schema 3; +- 134/134 checks passed; +- zero child and nested return codes; +- empty gate-failure arrays; +- clean source state before and after execution; +- SHA-256 `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`. + +Physical-GPU evidence: https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4 + +No universal GPU speedup claim is made. Performance depends on problem size, backend, hardware, tie method, and synchronization costs. + +## Upgrade notes and known limits + +- Python 3.9 or newer is required. +- Exact ties do not currently provide robust or cluster covariance. +- `PenalizedCoxPHModel` inference is not implemented in 0.2.3. +- Baseline prediction requires inference-enabled fitting. +- Two-stage and successive-halving CoxPHCV controls currently use the documented exhaustive single-pass safety strategy. +- Apple MPS is not currently a statgpu device backend. + +## Full change history + +- Main implementation: https://github.com/TheHiddenObserver/statgpu/pull/80 +- Release preparation: https://github.com/TheHiddenObserver/statgpu/pull/86 +- Full comparison: https://github.com/TheHiddenObserver/statgpu/compare/v0.2.2...v0.2.3 +- Repository changelog: https://github.com/TheHiddenObserver/statgpu/blob/master/CHANGELOG.md From d767cab07b4f5671ad638d29add540f19e505010 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:44:35 +0800 Subject: [PATCH 15/19] ci: require complete versioned GitHub release notes --- .github/workflows/release-notes.yml | 118 ++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/release-notes.yml diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 000000000..5ace632df --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,118 @@ +name: Release notes validation + +on: + pull_request: + branches: [master] + paths: + - "pyproject.toml" + - "statgpu/__init__.py" + - "CHANGELOG.md" + - "docs/en/changelog.md" + - "docs/cn/changelog.md" + - "RELEASING.md" + - ".github/releases/**" + - ".github/workflows/publish.yml" + - ".github/workflows/release-notes.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-release-notes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Validate versioned GitHub Release notes + run: | + python - <<'PY' + import pathlib + import re + import tomllib + + root = pathlib.Path.cwd() + pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) + version = pyproject["project"]["version"] + + init_text = (root / "statgpu/__init__.py").read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_text, re.M) + if match is None: + raise SystemExit("statgpu/__init__.py does not declare __version__") + if match.group(1) != version: + raise SystemExit( + f"version mismatch: pyproject.toml={version}, " + f"statgpu/__init__.py={match.group(1)}" + ) + + notes_path = root / ".github" / "releases" / f"v{version}.md" + if not notes_path.is_file(): + raise SystemExit(f"missing GitHub Release notes: {notes_path.relative_to(root)}") + + notes = notes_path.read_text(encoding="utf-8") + lines = notes.splitlines() + expected_title = f"# statgpu {version}" + if not lines or lines[0].strip() != expected_title: + raise SystemExit( + f"release notes must start with {expected_title!r}: " + f"{notes_path.relative_to(root)}" + ) + + required_sections = [ + "## Highlights", + "## Installation and platform support", + "## Validation", + "## Upgrade notes and known limits", + "## Full change history", + ] + missing_sections = [section for section in required_sections if section not in notes] + if missing_sections: + raise SystemExit( + "release notes are missing required sections: " + + ", ".join(missing_sections) + ) + + if len(notes.split()) < 500: + raise SystemExit("GitHub Release notes are too short to describe the release") + + forbidden_placeholders = ["TODO", "TBD", "X.Y.Z", "CHANGEME"] + present_placeholders = [token for token in forbidden_placeholders if token in notes] + if present_placeholders: + raise SystemExit( + "release notes contain unresolved placeholders: " + + ", ".join(present_placeholders) + ) + + required_fragments = [ + f"pip install statgpu=={version}", + f"...v{version}", + "Windows", + "macOS", + "Ubuntu", + ] + missing_fragments = [fragment for fragment in required_fragments if fragment not in notes] + if missing_fragments: + raise SystemExit( + "release notes are missing user-facing release details: " + + ", ".join(missing_fragments) + ) + + synchronized_files = [ + root / "CHANGELOG.md", + root / "docs" / "en" / "changelog.md", + root / "docs" / "cn" / "changelog.md", + ] + for path in synchronized_files: + text = path.read_text(encoding="utf-8") + if version not in text: + raise SystemExit( + f"{path.relative_to(root)} does not contain release version {version}" + ) + + print(f"validated {notes_path.relative_to(root)} ({len(notes.split())} words)") + PY From be6a1fdec9313fab07f5a7cd828e032402b19622 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:45:17 +0800 Subject: [PATCH 16/19] ci: publish GitHub Release from versioned notes --- .github/workflows/publish.yml | 81 +++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9e49632a6..805be1800 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,6 +10,7 @@ permissions: jobs: publish: + name: Publish distributions to PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -24,7 +25,7 @@ jobs: python -m pip install --upgrade pip python -m pip install build twine - - name: Verify tag and package versions + - name: Verify tag, package versions, and release notes run: | python - <<'PY' import os @@ -32,14 +33,15 @@ jobs: import re import tomllib + root = pathlib.Path.cwd() tag = os.environ["GITHUB_REF_NAME"] if not tag.startswith("v"): raise SystemExit(f"release tag must start with v: {tag}") tag_version = tag[1:] - pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) + pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) project_version = pyproject["project"]["version"] - init_text = pathlib.Path("statgpu/__init__.py").read_text(encoding="utf-8") + init_text = (root / "statgpu/__init__.py").read_text(encoding="utf-8") match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_text, re.M) if match is None: raise SystemExit("statgpu/__init__.py does not declare __version__") @@ -51,7 +53,29 @@ jobs: f"tag={tag_version}, pyproject.toml={project_version}, " f"statgpu/__init__.py={package_version}" ) - print(tag_version) + + notes_path = root / ".github" / "releases" / f"{tag}.md" + if not notes_path.is_file(): + raise SystemExit(f"missing GitHub Release notes: {notes_path.relative_to(root)}") + notes = notes_path.read_text(encoding="utf-8") + if not notes.startswith(f"# statgpu {tag_version}\n"): + raise SystemExit( + f"GitHub Release notes title does not match tag {tag}: " + f"{notes_path.relative_to(root)}" + ) + required_sections = [ + "## Highlights", + "## Installation and platform support", + "## Validation", + "## Upgrade notes and known limits", + "## Full change history", + ] + missing = [section for section in required_sections if section not in notes] + if missing: + raise SystemExit( + "GitHub Release notes are incomplete: " + ", ".join(missing) + ) + print(f"validated version {tag_version} and {notes_path.relative_to(root)}") PY - name: Build package (pure-Python wheel and sdist) @@ -87,8 +111,57 @@ jobs: print(statgpu.__version__) PY + - name: Retain validated release artifacts + uses: actions/upload-artifact@v4 + with: + name: statgpu-tagged-release-distributions + path: dist/* + if-no-files-found: error + retention-days: 14 + - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: python -m twine upload --non-interactive dist/* + + github-release: + name: Publish GitHub Release + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download validated release artifacts + uses: actions/download-artifact@v4 + with: + name: statgpu-tagged-release-distributions + path: dist + + - name: Create or update GitHub Release from versioned notes + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="$GITHUB_REF_NAME" + VERSION="${TAG#v}" + NOTES_FILE=".github/releases/${TAG}.md" + TITLE="statgpu ${VERSION}" + + test -f "$NOTES_FILE" + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release edit "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$TITLE" \ + --notes-file "$NOTES_FILE" + gh release upload "$TAG" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --clobber + else + gh release create "$TAG" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "$TITLE" \ + --notes-file "$NOTES_FILE" + fi From e43f9f3ec6e2bd622b33a52859d6ee73baeb3355 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:46:19 +0800 Subject: [PATCH 17/19] docs: make versioned GitHub release notes authoritative --- RELEASING.md | 116 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 23 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 8521e0450..dd9397aa1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,10 +1,13 @@ -# Releasing statgpu to PyPI +# Releasing statgpu to PyPI and GitHub This document is for maintainers preparing an official `statgpu` release. -The repository publishes from GitHub Actions when a tag matching `v*` is pushed. -The upload workflow is defined in [`.github/workflows/publish.yml`](.github/workflows/publish.yml), -and pull-request package validation is defined in -[`.github/workflows/release-package.yml`](.github/workflows/release-package.yml). +The repository publishes when a tag matching `v*` is pushed. + +The release automation is defined in: + +- [`.github/workflows/publish.yml`](.github/workflows/publish.yml) for PyPI and GitHub Release publication; +- [`.github/workflows/release-package.yml`](.github/workflows/release-package.yml) for wheel and sdist validation; +- [`.github/workflows/release-notes.yml`](.github/workflows/release-notes.yml) for versioned GitHub Release-note validation. ## Release model @@ -20,6 +23,22 @@ package version: 0.2.3 tag: v0.2.3 ``` +Each release also has one authoritative GitHub Release body: + +```text +.github/releases/vX.Y.Z.md +``` + +For example, the GitHub Release notes for 0.2.3 are stored at: + +```text +.github/releases/v0.2.3.md +``` + +The tag workflow publishes this file verbatim as the GitHub Release body. Do not +rely on GitHub's automatically generated PR list as the primary release notes, +and do not compose the final release body manually in the GitHub UI. + PyPI release files are immutable. A broken upload cannot be replaced under the same version; prepare a new patch version instead. @@ -28,19 +47,31 @@ same version; prepare a new patch version instead. Start from the latest `master` after the intended feature and fix pull requests are merged. -Update both version declarations and the release-facing documentation: +Update both version declarations and all release-facing sources: - `pyproject.toml`; - `statgpu/__init__.py`; +- `.github/releases/vX.Y.Z.md`; - `CHANGELOG.md`; - `docs/en/changelog.md`; - `docs/cn/changelog.md`; -- README or model documentation when installation, compatibility, or public - behavior changed. +- README or model documentation when installation, compatibility, limitations, + or public behavior changed. + +The versioned GitHub Release document must be user-facing. It should explain: + +- what major capability was added or changed; +- which public APIs and workflows are affected; +- installation and platform support; +- behavioral changes and upgrade implications; +- known limitations and unsupported combinations; +- validation evidence without turning the document into an internal audit log; +- links to the main implementation PR, release PR, version comparison, and + repository changelog. Keep release-only changes separate from implementation work. A release pull -request should primarily contain version, packaging, changelog, and -release-validation changes. +request should primarily contain version, packaging, changelog, release notes, +and release-validation changes. ## 2. Validate the release pull request @@ -49,6 +80,8 @@ contracts, documentation contracts, and the Python 3.9–3.12 regression matrix. For changes affecting CuPy, Torch, inference, device routing, or performance, record physical-GPU acceptance on the exact release source commit. +### Package validation + The `Release package validation` workflow automatically: 1. checks that `pyproject.toml` and `statgpu/__init__.py` declare the same version; @@ -71,6 +104,21 @@ The cross-platform matrix validates portability of the published `py3-none-any` CPU wheel. It does not claim Apple MPS support or replace the separate physical-NVIDIA-GPU acceptance required for CUDA behavior. +### GitHub Release-note validation + +The `Release notes validation` workflow requires: + +- `.github/releases/vX.Y.Z.md` matching the package version; +- a title of the form `# statgpu X.Y.Z`; +- substantive Highlights, Installation and platform support, Validation, + Upgrade notes and known limits, and Full change history sections; +- no unresolved `TODO`, `TBD`, `X.Y.Z`, or similar placeholders; +- explicit installation, platform, and version-comparison information; +- the same version to appear in the root, English, and Chinese changelogs. + +This gate prevents a release tag from being prepared with a generic or +incomplete GitHub Release description. + For a local rehearsal, run: ```bash @@ -108,7 +156,8 @@ TestPyPI and PyPI use separate credentials. Before merging, verify: - both version declarations match the intended release; -- all release notes are accurate and synchronized in English and Chinese; +- `.github/releases/vX.Y.Z.md` accurately describes the user-visible release; +- root, English, and Chinese changelogs are synchronized; - required GitHub Actions jobs are green on the exact release head; - required physical-GPU evidence is recorded for the exact source commit; - wheel and sdist validation, the Ubuntu sdist clean-install check, and the @@ -135,18 +184,28 @@ Pushing the tag starts `Publish to PyPI`. The workflow: 1. checks out the tagged commit; 2. verifies that the tag, `pyproject.toml`, and `statgpu.__version__` agree; -3. builds a pure-Python wheel and sdist with `STATGPU_NO_EXT=1`; -4. runs `twine check`; -5. installs the wheel in a clean environment and checks its version and core +3. verifies that `.github/releases/vX.Y.Z.md` exists and has the required + versioned sections; +4. builds a pure-Python wheel and sdist with `STATGPU_NO_EXT=1`; +5. runs `twine check`; +6. installs the wheel in a clean environment and checks its version and core imports; -6. uploads `dist/*` to PyPI using the repository secret `PYPI_TOKEN`. +7. retains the validated distributions as a workflow artifact; +8. uploads the distributions to PyPI using the repository secret `PYPI_TOKEN`; +9. only after the PyPI job succeeds, creates or updates the GitHub Release using + `.github/releases/vX.Y.Z.md` as the exact release body and attaches the same + wheel and sdist. + +PyPI publication and GitHub Release creation are separate jobs. If the GitHub +Release job fails after PyPI succeeds, rerun only the failed job; the successful +PyPI upload does not need to be repeated. Never place the PyPI token in source files, committed command output, issues, pull requests, or documentation examples. ## 6. Verify the published release -After the workflow succeeds, verify the release from a new environment: +After the workflow succeeds, verify the PyPI package from a new environment: ```bash python -m venv /tmp/statgpu-pypi-test @@ -168,27 +227,38 @@ Also verify: - the sdist is present; - dependency extras and supported Python versions are correct; - project, documentation, issue, and changelog links work; -- a GitHub Release is created from the same tag using the changelog as the basis - for release notes. +- the GitHub Release title is `statgpu X.Y.Z`; +- the GitHub Release body matches `.github/releases/vX.Y.Z.md` rather than an + automatically generated PR summary; +- the GitHub Release includes the same wheel and sdist published by the tag + workflow. ## 7. Failure handling -### Version mismatch +### Version or release-note mismatch -The publish workflow stops before upload. Correct the version in a new commit, -merge a new release PR, and create a new tag. Do not move a published tag. +The publish workflow stops before upload. Correct the version or release-note +file in a new commit, merge a new release PR, and create a new tag. Do not move a +published tag. -### Partial upload +### Partial PyPI upload PyPI may accept one artifact before another fails. Because filenames and versions are immutable, inspect the release and normally issue a new patch version rather than trying to replace the accepted file. +### GitHub Release publication failure + +If the PyPI job succeeded and only the GitHub Release job failed, rerun the +failed GitHub Release job. It is idempotent: an existing release is updated from +the versioned notes file and attached artifacts are uploaded with replacement. + ### Bad release already published - yank the PyPI release when appropriate; - fix the problem in a new patch release; -- document the incident and migration path in the changelog; +- document the incident and migration path in the changelog and versioned release + notes; - do not rewrite Git history or reuse the released version. ## Future infrastructure improvement From b1e059c5c0b4283d3bfd213a020afcde58bd8997 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:47:16 +0800 Subject: [PATCH 18/19] docs: record authoritative GitHub release publication --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0131a4234..282c12a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,12 @@ All notable changes to statgpu are documented here, organized by release and dat ### Validation - Hosted workflow #960 passed on the final reviewed head `f05a44ad363b46612e956e137e2f00d040765acb`: documentation, static, full CPU, and Python 3.9–3.12 regression jobs all passed; the complete CPU suite reported 1881 passed and 662 skipped. - The final exact-head physical-GPU promotion artifact is published at https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4. It records schema 3, 134/134 passing checks, zero child and nested return codes, empty gate-failure arrays, clean source state before and after execution, and SHA-256 `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`. -- Added release-package validation that checks version consistency, builds the pure-Python wheel and sdist, runs `twine check`, validates artifact contents, and smoke-installs both distributions in clean environments. +- Added release-package validation that checks version consistency, builds the pure-Python wheel and sdist, runs `twine check`, validates artifact contents, clean-installs the sdist on Ubuntu, and clean-installs the same wheel on Ubuntu, Windows, and macOS. -### Packaging +### Packaging and release publication - Bumped the package version to `0.2.3` in `pyproject.toml` and `statgpu/__init__.py`. - The official wheel remains a universal `py3-none-any` artifact built with `STATGPU_NO_EXT=1`; optional Cython sources remain available in the sdist. +- Added the authoritative GitHub Release document at `.github/releases/v0.2.3.md`, a release-note completeness gate, and tag automation that publishes that file as the GitHub Release body after the PyPI job succeeds. ## Earlier history From e30f6d8bb4499a4f5398e4c6a044e73f5ddc0a53 Mon Sep 17 00:00:00 2001 From: Ziqian Lin <51812297+TheHiddenObserver@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:48:35 +0800 Subject: [PATCH 19/19] ci: include versioned release notes in package gate --- .github/workflows/release-package.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 1f7e8163b..6c6299370 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -13,8 +13,10 @@ on: - "docs/en/changelog.md" - "docs/cn/changelog.md" - "RELEASING.md" + - ".github/releases/**" - ".github/workflows/publish.yml" - ".github/workflows/release-package.yml" + - ".github/workflows/release-notes.yml" workflow_dispatch: permissions: