Skip to content

Commit 20d1b15

Browse files
Merge pull request #3 from aliengineering-byte/codex/pypi-trusted-publishing
Add trusted PyPI publishing workflow
2 parents 072bedf + 015eac0 commit 20d1b15

3 files changed

Lines changed: 201 additions & 11 deletions

File tree

.github/workflows/publish-pypi.yml

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
name: Publish an existing GitHub release to PyPI
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tag:
7+
description: Immutable release tag (for example, v0.2.0)
8+
required: true
9+
type: string
10+
version:
11+
description: Expected package version (for example, 0.2.0)
12+
required: true
13+
type: string
14+
commit:
15+
description: Expected 40-character commit for the tag
16+
required: true
17+
type: string
18+
wheel_sha256:
19+
description: Expected SHA-256 for the wheel release asset
20+
required: true
21+
type: string
22+
sdist_sha256:
23+
description: Expected SHA-256 for the source release asset
24+
required: true
25+
type: string
26+
27+
permissions: {}
28+
29+
concurrency:
30+
group: pypi-${{ inputs.tag }}
31+
cancel-in-progress: false
32+
33+
jobs:
34+
verify:
35+
name: Verify immutable release assets
36+
if: >-
37+
github.repository == 'aliengineering-byte/phaseprobe' &&
38+
github.ref == 'refs/heads/main'
39+
runs-on: ubuntu-latest
40+
permissions: {}
41+
steps:
42+
- name: Download and verify the selected release
43+
env:
44+
TAG: ${{ inputs.tag }}
45+
VERSION: ${{ inputs.version }}
46+
EXPECTED_COMMIT: ${{ inputs.commit }}
47+
WHEEL_SHA256: ${{ inputs.wheel_sha256 }}
48+
SDIST_SHA256: ${{ inputs.sdist_sha256 }}
49+
shell: bash
50+
run: |
51+
set -euo pipefail
52+
53+
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
54+
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
55+
[[ "$TAG" == "v$VERSION" ]]
56+
[[ "$EXPECTED_COMMIT" =~ ^[0-9a-f]{40}$ ]]
57+
[[ "$WHEEL_SHA256" =~ ^[0-9a-f]{64}$ ]]
58+
[[ "$SDIST_SHA256" =~ ^[0-9a-f]{64}$ ]]
59+
60+
repository_url="https://github.com/${GITHUB_REPOSITORY}.git"
61+
tag_refs="$(git ls-remote "$repository_url" "refs/tags/$TAG" "refs/tags/$TAG^{}")"
62+
direct_commit="$(awk -v ref="refs/tags/$TAG" '$2 == ref { print $1 }' <<<"$tag_refs")"
63+
peeled_commit="$(awk -v ref="refs/tags/$TAG^{}" '$2 == ref { print $1 }' <<<"$tag_refs")"
64+
resolved_commit="${peeled_commit:-$direct_commit}"
65+
[[ -n "$resolved_commit" ]]
66+
[[ "$resolved_commit" == "$EXPECTED_COMMIT" ]]
67+
68+
wheel="phaseprobe-$VERSION-py3-none-any.whl"
69+
sdist="phaseprobe-$VERSION.tar.gz"
70+
release_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/$TAG"
71+
mkdir dist
72+
curl --fail --location --proto '=https' --tlsv1.2 \
73+
--output "dist/$wheel" "$release_url/$wheel"
74+
curl --fail --location --proto '=https' --tlsv1.2 \
75+
--output "dist/$sdist" "$release_url/$sdist"
76+
printf '%s %s\n%s %s\n' \
77+
"$WHEEL_SHA256" "dist/$wheel" \
78+
"$SDIST_SHA256" "dist/$sdist" | sha256sum --check --strict
79+
80+
python3 -m venv .twine-check
81+
.twine-check/bin/python -m pip install --disable-pip-version-check 'twine==7.0.0'
82+
.twine-check/bin/python -m twine check "dist/$wheel" "dist/$sdist"
83+
84+
python3 - "$VERSION" "dist/$wheel" "dist/$sdist" <<'PY'
85+
import email
86+
import sys
87+
import tarfile
88+
import zipfile
89+
90+
expected_version, wheel_path, sdist_path = sys.argv[1:]
91+
92+
with zipfile.ZipFile(wheel_path) as archive:
93+
metadata_names = [
94+
name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
95+
]
96+
assert len(metadata_names) == 1, metadata_names
97+
wheel_metadata = email.message_from_bytes(archive.read(metadata_names[0]))
98+
99+
with tarfile.open(sdist_path, "r:gz") as archive:
100+
metadata_members = [
101+
member
102+
for member in archive.getmembers()
103+
if member.isfile() and member.name.count("/") == 1 and member.name.endswith("/PKG-INFO")
104+
]
105+
assert len(metadata_members) == 1, [member.name for member in metadata_members]
106+
extracted = archive.extractfile(metadata_members[0])
107+
assert extracted is not None
108+
sdist_metadata = email.message_from_bytes(extracted.read())
109+
110+
for metadata in (wheel_metadata, sdist_metadata):
111+
assert metadata["Name"] == "phaseprobe"
112+
assert metadata["Version"] == expected_version
113+
assert metadata["Requires-Python"] == ">=3.10"
114+
assert metadata["License-Expression"] == "Apache-2.0"
115+
assert "scipy" in metadata.get_all("Provides-Extra", [])
116+
requirements = metadata.get_all("Requires-Dist", [])
117+
assert any(req.startswith("numpy") and "extra == 'scipy'" in req for req in requirements)
118+
assert any(req.startswith("scipy") and "extra == 'scipy'" in req for req in requirements)
119+
project_urls = metadata.get_all("Project-URL", [])
120+
assert any(url.startswith("Source, https://github.com/aliengineering-byte/phaseprobe") for url in project_urls)
121+
PY
122+
123+
- name: Store the verified distributions
124+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
125+
with:
126+
name: verified-distributions-${{ inputs.tag }}
127+
path: dist/*
128+
if-no-files-found: error
129+
retention-days: 1
130+
131+
publish:
132+
name: Publish verified distributions
133+
needs: verify
134+
runs-on: ubuntu-latest
135+
environment:
136+
name: pypi
137+
url: https://pypi.org/project/phaseprobe/${{ inputs.version }}/
138+
permissions:
139+
id-token: write
140+
steps:
141+
- name: Retrieve the verified distributions
142+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
143+
with:
144+
name: verified-distributions-${{ inputs.tag }}
145+
path: dist
146+
147+
- name: Publish distributions to PyPI
148+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
149+
with:
150+
packages-dir: dist/

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Maintaining a simulation is risky when a tiny parameter or initial-condition change can cross a qualitative boundary while ordinary numeric assertions still look plausible. PhaseProbe runs a bounded, deterministic search, records exactly what it tested, and emits an offline report plus an executable pytest regression.
66

77
```console
8-
$ python -m pip install .
8+
$ pip install phaseprobe
99
$ phaseprobe scan --example logistic
1010
QUALITATIVE TRANSITION FOUND
1111

@@ -24,12 +24,19 @@ $ python -m pytest -q tests/generated
2424

2525
No API key, LLM, GPU, Docker, account, telemetry, network connection, or hosted service is required at runtime. PhaseProbe's base installation has no third-party runtime dependencies; NumPy and SciPy are isolated in the optional `scipy` extra.
2626

27+
## Installation
28+
29+
```bash
30+
pip install phaseprobe
31+
pip install "phaseprobe[scipy]"
32+
```
33+
2734
## Five-minute quick start
2835

2936
Requires Python 3.10 or newer on Windows or Linux.
3037

3138
```bash
32-
python -m pip install .
39+
pip install phaseprobe
3340
phaseprobe scan --example logistic
3441
phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
3542
phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json

docs/upstream/SCIPY_INTEGRATION_PROPOSAL.md

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,48 @@ propose this external qualitative regression/replay layer.
9595
Project quick start and terminal assets:
9696
https://github.com/aliengineering-byte/phaseprobe#use-with-scipy
9797

98-
## Suggested discussion title
98+
## Discussion title and body
9999

100-
`PhaseProbe: tolerance-aware qualitative regression fixtures for SciPy solve_ivp models`
100+
`PhaseProbe: reproducible transition detection and regression generation for solve_ivp models`
101101

102-
## Human-editable outreach message
102+
Hi SciPy community,
103103

104-
> I built an optional, dependency-isolated PhaseProbe adapter for public `solve_ivp` models. It
105-
> keeps SciPy responsible for integration and adds bounded qualitative searches, explicit
106-
> tolerance replay, and generated pytest regressions. The examples include finite-time Lorenz
107-
> divergence with a negative control and a Lotka–Volterra invariant refinement check. I reviewed
108-
> related tolerance, event, work-bound, and callback discussions and do not propose a SciPy API
109-
> change. Would this workflow be useful to users maintaining scientific simulation regressions?
104+
I built PhaseProbe, an open-source downstream testing tool for dynamical-system simulations. It
105+
uses `scipy.integrate.solve_ivp` through a trajectory-level adapter to search bounded parameter or
106+
initial-condition regions for reproducible qualitative transitions, preserve replay evidence, and
107+
generate pytest regression tests from the findings.
108+
109+
The motivation is a practical testing problem: a numerical model can continue integrating
110+
successfully while a small parameter or initial-condition change moves it into a different
111+
qualitative regime. PhaseProbe records the search configuration, solver method and tolerances,
112+
environment evidence, trajectory hashes, observables, classifications, and transition bracket so
113+
that the result can be independently replayed.
114+
115+
The current release includes examples for Lorenz finite-time divergence and predator–prey
116+
invariant drift. For adaptive SciPy integrations, replay is tolerance-based rather than claimed to
117+
be bit-exact. The results are explicitly bounded, finite-time numerical evidence—not proofs of
118+
chaos, exact bifurcation locations, Lyapunov estimates, or globally minimal perturbations.
119+
120+
Repository:
121+
https://github.com/aliengineering-byte/phaseprobe
122+
123+
Release:
124+
https://github.com/aliengineering-byte/phaseprobe/releases/tag/v0.2.0
125+
126+
PyPI:
127+
https://pypi.org/project/phaseprobe/
128+
129+
Technical proposal and limitations:
130+
https://github.com/aliengineering-byte/phaseprobe/blob/v0.2.0/docs/upstream/SCIPY_INTEGRATION_PROPOSAL.md
131+
132+
I am not proposing to add PhaseProbe to SciPy core. I would value feedback on three points:
133+
134+
1. Whether the tolerance-based replay evidence for adaptive `solve_ivp` trajectories is
135+
scientifically and practically appropriate.
136+
2. Whether the trajectory-level adapter and recorded solver/environment metadata omit evidence
137+
that SciPy users would expect.
138+
3. Whether a downstream example or ecosystem/documentation reference could be appropriate if the
139+
tool proves useful to users.
140+
141+
Feedback on the design, terminology, numerical claims, and useful real-world test cases would be
142+
very welcome.

0 commit comments

Comments
 (0)