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