diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 02f125f..b0eb5a5 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -6,11 +6,19 @@
# library and the standalone driver. The x86-64 wheels target x86-64-v3 (AVX2), as the static
# Windows release does. Each wheel is installed into a clean environment and
# smoke-tested (release/smoke_test_wheel.py) on its own platform. The distributions are workflow
-# artifacts; publishing them is a separate, explicit step.
+# artifacts. They are uploaded only by the two publish jobs, through PyPI Trusted Publishing
+# (no stored token): to TestPyPI when a manual run asks for it, and to PyPI when a GitHub
+# release is published, after checking that the distributions carry the release tag's version.
name: wheels
on:
workflow_dispatch:
+ inputs:
+ upload:
+ description: Upload the built distributions (TestPyPI only; PyPI uploads come from releases)
+ type: choice
+ options: [none, testpypi]
+ default: none
release:
types: [published]
pull_request:
@@ -189,3 +197,69 @@ jobs:
name: cabledyn-wheel-windows-x86_64
path: wheelhouse/*.whl
if-no-files-found: error
+
+ publish-testpypi:
+ # manual runs that ask for it; a version already on TestPyPI is skipped, not an error
+ if: github.event_name == 'workflow_dispatch' && inputs.upload == 'testpypi'
+ needs: [sdist, linux, macos, windows]
+ runs-on: ubuntu-latest
+ environment:
+ name: testpypi
+ url: https://test.pypi.org/p/cabledyn
+ permissions:
+ id-token: write
+ steps:
+ - name: Download the distributions
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: cabledyn-*
+ merge-multiple: true
+ path: dist
+
+ - name: Check the distribution set
+ run: |
+ ls -l dist
+ test "$(ls dist/*.tar.gz | wc -l)" -eq 1
+ test "$(ls dist/*.whl | wc -l)" -eq 3
+
+ - name: Publish to TestPyPI
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
+ with:
+ repository-url: https://test.pypi.org/legacy/
+ skip-existing: true
+
+ publish-pypi:
+ # published GitHub releases only, behind the pypi environment
+ if: github.event_name == 'release'
+ needs: [sdist, linux, macos, windows]
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/cabledyn
+ permissions:
+ id-token: write
+ steps:
+ - name: Download the distributions
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: cabledyn-*
+ merge-multiple: true
+ path: dist
+
+ - name: Check the distributions against the release tag
+ env:
+ TAG: ${{ github.event.release.tag_name }}
+ run: |
+ ls -l dist
+ version="${TAG#v}"
+ test "$(ls dist/*.tar.gz | wc -l)" -eq 1
+ test "$(ls dist/*.whl | wc -l)" -eq 3
+ for f in dist/*; do
+ case "$(basename "$f")" in
+ "cabledyn-${version}.tar.gz"|"cabledyn-${version}-py3-none-"*.whl) ;;
+ *) echo "::error::$(basename "$f") is not version ${version} of release ${TAG}"; exit 1 ;;
+ esac
+ done
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a26e3a..3d2fb7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,7 +43,9 @@ All notable changes to CableDyn are recorded in this file. The format follows
- Release wheels built in CI (`.github/workflows/wheels.yml`): Linux x86-64 (`manylinux_2_28`,
x86-64-v3), macOS arm64 and Windows x86-64 (IFX, static, the release's reference LAPACK), plus
the sdist. Each wheel is installed into a clean environment and smoke-tested
- (`release/smoke_test_wheel.py`); publishing is a separate step.
+ (`release/smoke_test_wheel.py`). The same workflow publishes them through PyPI Trusted
+ Publishing: to TestPyPI on request from a manual run, and to PyPI when a GitHub release is
+ published (after checking the release tag's version and approval of the `pypi` environment).
### Changed
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 8a908be..2f7bfa8 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -137,8 +137,11 @@ A wheel built this way still needs the toolchain's runtime and LAPACK libraries
search path; the release wheels bundle them (or link them statically on Windows).
The release wheels come from `.github/workflows/wheels.yml` (manual runs, published
-releases, and pull requests that touch the packaging), as workflow artifacts; publishing
-them is a separate step. It builds the sdist (checked with `twine`) and three wheels:
+releases, and pull requests that touch the packaging), as workflow artifacts, which only
+its publish jobs upload (TestPyPI from a manual run with **upload = testpypi**, PyPI from a
+published release; see
+[RELEASE.md](https://github.com/SMI-Lab-Inha/CableDyn/blob/main/RELEASE.md)). It builds
+the sdist (checked with `twine`) and three wheels:
| Wheel | Toolchain | Runtime and LAPACK | Build |
|---|---|---|---|
diff --git a/RELEASE.md b/RELEASE.md
index 9136568..ded58fe 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -119,6 +119,19 @@ manually (`workflow_dispatch`) as a preflight. Download its
`CableDyn-release-assets` artifact, verify every checksum (on a second machine
where practical), and keep the workflow URL with the release record.
+The PyPI distribution (the sdist and the Linux, macOS and Windows wheels that carry
+the library and the driver) comes from `.github/workflows/wheels.yml`. As its
+preflight, run that workflow manually with **upload = testpypi**: it builds and
+smoke-tests the four distributions and uploads them to TestPyPI (a version already
+there is skipped). Then, in a clean environment on each platform you can reach:
+
+```powershell
+python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "cabledyn==MAJOR.MINOR.PATCH"
+python release/smoke_test_wheel.py examples/dynamic_chain_current.dat --expect-build x86-64-v3
+```
+
+(`--expect-build baseline` on macOS arm64.)
+
## 5. Tag and publish
Tag only the commit that passed every check:
@@ -141,9 +154,16 @@ partial release or invalid manifest fails without replacing any file. If assets
not match, withdraw the release and investigate; never replace a file without a
documented correction.
+Publishing also starts `.github/workflows/wheels.yml`, which rebuilds and smoke-tests
+the PyPI distributions from the tag, checks that every file carries the tag's
+version, and then waits for approval of the `pypi` environment. Approve it only
+after the release assets above check out: a version uploaded to PyPI can be yanked
+but never replaced.
+
## 6. Close the release
-Confirm that the release page lists all five assets, the documentation builds from
+Confirm that the release page lists all five assets,
+lists the sdist and the three wheels of the release, the documentation builds from
the tag, the GitHub source archives contain no credentials or run output, and a
fresh clone passes the fast test tier. Then add an empty `[Unreleased]` section to
the changelog.
@@ -162,6 +182,12 @@ Before the first release is published from a new GitHub repository:
- [ ] **Actions**: allow GitHub-hosted runners; the workflows need no secrets
beyond the automatic `GITHUB_TOKEN`, and only the release job writes
(`contents: write`).
+- [ ] **PyPI**: on and , register a
+ (pending) Trusted Publisher for the project `cabledyn`: this repository, the
+ workflow `wheels.yml`, and the environments `pypi` and `testpypi`
+ respectively. Create both environments in the repository settings and make
+ a maintainer a required reviewer of `pypi`. The publish jobs then need no
+ token (`id-token: write` only).
- [ ] **Branch protection** on `main` and `dev`: require pull requests, the `fortran` and
`python` workflow checks, and linear history.
- [ ] **Read the Docs**: import the repository as project `cabledyn`, build