From e7961e1ebb5296144f523cbf9ebf2d5d88fa418a Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 3 Sep 2026 15:08:00 +0200 Subject: [PATCH 1/4] ci: add PyPI trusted-publish workflow, manual docs dispatch Adds .github/workflows/publish.yml: on a published GitHub Release, build sdist+wheel from the release tag, check metadata with twine, and publish to PyPI via pypa/gh-action-pypi-publish using OIDC trusted publishing (permissions: id-token: write) -- no stored token/secret. Scoped to a "pypi" GitHub Environment so PyPI's trusted-publisher check is exact (repo + workflow + environment) and so manual-approval gating can be added later if wanted. Requires a one-time manual step on PyPI: add a trusted publisher on the existing pgraph-python project (Settings -> Publishing) naming this repo, workflow filename, and the "pypi" environment. Also adds workflow_dispatch to ci.yml's triggers, and widens the docs job's condition to run on manual dispatch as well as push, so the Sphinx docs can be manually rebuilt/redeployed on demand without any relationship to a PyPI release -- the two are on separate triggers entirely (push+dispatch for docs, release-published only for PyPI). Verified: pyproject.toml/publish.yml YAML parses; `python -m build` produces a clean sdist+wheel from this checkout; `twine check` passes on both; a genuinely isolated venv (confirmed via sys.prefix) installs the built wheel and successfully imports and exercises real functionality (UGraph/add_vertex/add_edge/__str__). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 3 ++- .github/workflows/publish.yml | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a35cfeab..641c841b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: jobs: unittest: @@ -81,7 +82,7 @@ jobs: verbose: true docs: - if: github.event_name == 'push' + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' needs: unittest runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..944f9fe0 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,45 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install build tooling + run: | + python -m pip install --upgrade pip + pip install build twine + - name: Build sdist and wheel + run: python -m build + - name: Check metadata + run: twine check dist/* + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/pgraph-python/ + permissions: + id-token: write # required for PyPI trusted publishing (OIDC) + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From 4cd0d6559ecf6765e9f5a9f83c9d2eb1f2be5a81 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 3 Sep 2026 15:09:28 +0200 Subject: [PATCH 2/4] Remove obsolete twine-upload Makefile target make pypi (build + twine upload + tag + push) is superseded by .github/workflows/publish.yml's trusted-publish-on-release flow. Updated the help text accordingly. Co-Authored-By: Claude Sonnet 5 --- Makefile | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Makefile b/Makefile index c9dbb983..8cc2259a 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,8 @@ help: @echo " make docs - build Sphinx documentation" @echo " make docupdate - upload Sphinx documentation to GitHub pages" @echo " make dist - build dist files" - @echo " make upload - upload to PyPI" @echo " make clean - remove dist and docs build files" + @echo " (PyPI publish is via GitHub Releases, see .github/workflows/publish.yml)" @echo " make help - this message$(BLACK)" test: @@ -34,17 +34,6 @@ dist: .FORCE #$(MAKE) test python -m build -pypi: .FORCE - validate-pyproject pyproject.toml - @if ! git diff --quiet || ! git diff --cached --quiet; then \ - echo "Error: uncommitted changes present, aborting upload"; exit 1; \ - fi - $(eval VERSION := $(shell grep '^version' pyproject.toml | sed 's/version = "\(.*\)"/\1/')) - python -m build - twine upload dist/* - git tag v$(VERSION) - git push origin v$(VERSION) - clean: .FORCE (cd docs; make clean) -rm -r *.egg-info From 471fcb355a681bfdaefacf6ef8da9588fe8ce834 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 10:33:15 +1000 Subject: [PATCH 3/4] ci: publish to PyPI from a GitHub release, by trusted publishing publish.yml follows the release workflow of the other toolboxes: verify that the tag is the version in pyproject.toml and that the tagged commit is on main, build the sdist and wheel, twine check, install the wheel into a clean environment and run a path query, then upload with OIDC from the pypi environment. skip_existing makes a re-run harmless. The default token is read-only. The docs job of ci.yml can be run by hand, from main only. The manual twine-upload Makefile target is gone, as a release does it. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 107 ++++++++++++++++++++++++++++------ 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 641c841b..e3b46ecf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: verbose: true docs: - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') needs: unittest runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 944f9fe0..15cfb45d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,45 +1,114 @@ +# Build and publish to PyPI when a GitHub release is created, or on manual trigger. +# Uses OIDC trusted publishing, so no PyPI token is stored. The trusted publisher +# on PyPI is: owner petercorke, repository pgraph-python, workflow publish.yml, +# environment pypi. Pure Python package, so one universal wheel and an sdist. +# +# To release: make sure main has the version in pyproject.toml, then create a +# GitHub release whose tag is v on main. + name: Publish to PyPI on: release: - types: [published] + types: [created] + workflow_dispatch: + +permissions: + contents: read jobs: + verify_version: + name: Verify tag matches package version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Check version + # Skip on manual workflow_dispatch runs (no release tag exists) + if: github.event_name == 'release' + run: | + TAG="${GITHUB_REF_NAME#v}" + PKG=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + echo "Tag version : $TAG" + echo "Package version: $PKG" + [ "$TAG" = "$PKG" ] || { echo "ERROR: tag $GITHUB_REF_NAME does not match pyproject.toml version $PKG"; exit 1; } + - name: Check the tagged commit is on main + if: github.event_name == 'release' + run: | + git fetch origin main + git merge-base --is-ancestor "$GITHUB_SHA" origin/main \ + || { echo "ERROR: $GITHUB_SHA is not on main, so it was not reviewed and tested there"; exit 1; } + build: + name: Build sdist and wheel + needs: verify_version runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 with: python-version: "3.12" - - name: Install build tooling - run: | - python -m pip install --upgrade pip - pip install build twine + - name: Build sdist and wheel - run: python -m build + run: pipx run build + - name: Check metadata - run: twine check dist/* - - name: Upload build artifacts - uses: actions/upload-artifact@v4 + run: pipx run twine check dist/* + + - uses: actions/upload-artifact@v7 with: name: dist path: dist/ - publish: + smoke_test: + name: Smoke-test wheel needs: build runs-on: ubuntu-latest + steps: + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - uses: actions/download-artifact@v8 + with: + name: dist + path: dist + + - name: Install the wheel (pulls dependencies from PyPI) + run: pip install dist/*.whl + + - name: Smoke test + run: | + python - <<'PY' + from importlib.metadata import version + import pgraph + + g = pgraph.UGraph() + g.add_vertex(coord=[0, 0], name="a") + g.add_vertex(coord=[3, 4], name="b") + g.add_edge("a", "b") + path, length, _ = g.path_Astar("a", "b") + assert length == 5.0, length + print("OK", version("pgraph-python"), length) + PY + + upload_pypi: + name: Publish to PyPI + needs: [build, smoke_test] + runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/project/pgraph-python/ permissions: - id-token: write # required for PyPI trusted publishing (OIDC) + id-token: write # required for OIDC trusted publishing steps: - - name: Download build artifacts - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: dist - path: dist/ - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + path: dist + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip_existing: true From 0c9b6fe41329dbc238799da95a162ae08b8f58fc Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 13 Oct 2026 10:35:42 +1000 Subject: [PATCH 4/4] ci: pin the PyPI publish action to a commit A third-party action with the right to publish should be an immutable reference. dependabot (github-actions, monthly) keeps it current. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/publish.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 15cfb45d..213c7d39 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -109,6 +109,7 @@ jobs: name: dist path: dist - - uses: pypa/gh-action-pypi-publish@release/v1 + # pinned to a commit, as this action can publish to PyPI; dependabot updates it + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: skip_existing: true