From b713e50591cdef8f5bb74fab52955908a5da50d1 Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 13:56:20 -0700 Subject: [PATCH 1/8] ci: publish to PyPI via OIDC trusted publishing on v* tags Add a GitHub Actions workflow that builds the sdist + wheel and publishes to PyPI using OIDC trusted publishing (no stored API tokens) whenever a v* tag is pushed. - build job verifies the tag matches the pyproject.toml version, builds, and runs twine check before uploading the artifact - publish job uses pypa/gh-action-pypi-publish with id-token: write and a 'pypi' environment - submodule is intentionally not checked out; cloudglue/sdk is committed so the build needs no SSH submodule access in CI --- .github/workflows/publish.yml | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..d1d99af0 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,67 @@ +name: Publish to PyPI + +# Publishes to PyPI via OIDC trusted publishing whenever a v* tag is pushed. +# No API tokens required — PyPI verifies the GitHub OIDC identity instead. +# +# git tag v0.7.15 +# git push origin v0.7.15 + +on: + push: + tags: + - "v*" + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Submodule (spec/) is not needed — cloudglue/sdk is already committed. + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tooling + run: python -m pip install --upgrade build + + - name: Verify tag matches pyproject version + run: | + PKG_VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')" + TAG_VERSION="${GITHUB_REF_NAME#v}" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "::error::Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match pyproject version $PKG_VERSION" >&2 + exit 1 + fi + echo "Building and publishing version $PKG_VERSION" + + - name: Build sdist and wheel + run: python -m build + + - name: Check artifacts + run: | + python -m pip install --upgrade twine + python -m twine check dist/* + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/cloudglue + permissions: + id-token: write # required for OIDC trusted publishing + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - uses: pypa/gh-action-pypi-publish@release/v1 From 190a0ebeeb8ca7140e2d0899e0fb1da93ceaa9e3 Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 13:57:57 -0700 Subject: [PATCH 2/8] ci: create GitHub Release with sdist + wheel on each tag After a successful PyPI publish, create a GitHub Release for the tag with auto-generated notes and the built .tar.gz + .whl attached as assets. - new github-release job runs only after publish succeeds - contents: write permission scoped to this job - softprops/action-gh-release uses the triggering tag automatically --- .github/workflows/publish.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d1d99af0..e00a2b74 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -65,3 +65,21 @@ jobs: path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + name: Create GitHub Release + needs: publish # only release after a successful PyPI publish + runs-on: ubuntu-latest + permissions: + contents: write # required to create the release and upload assets + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Create release with sdist + wheel attached + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true From f315df9984950baad9b61b4057e652cb70d6e8e6 Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 13:58:52 -0700 Subject: [PATCH 3/8] docs: document the tag-based release flow in README Add a Releasing section covering the OIDC tag-push flow (build -> PyPI publish -> GitHub Release) and the required one-time setup, while keeping the manual make build / make publish token path documented as a fallback. --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index 708dce6c..0a3c3bf9 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,52 @@ make generate # Generate SDK from OpenAPI spec make build # Build the package ``` +### Releasing + +Releases are published to PyPI automatically by the +[`Publish to PyPI`](.github/workflows/publish.yml) GitHub Actions workflow +whenever a `v*` tag is pushed. It uses +[OIDC trusted publishing](https://docs.pypi.org/trusted-publishers/) — no API +tokens are stored anywhere. + +To cut a release: + +```bash +# 1. Bump the version in pyproject.toml (e.g. 0.7.15 -> 0.7.16) and commit it. +# 2. Tag the commit. The tag MUST match the pyproject.toml version, prefixed +# with "v" — the workflow fails fast if they differ. +git tag v0.7.16 +git push origin v0.7.16 +``` + +Pushing the tag triggers, in order: + +1. **build** — verifies the tag matches `pyproject.toml`, builds the sdist + + wheel, runs `twine check`. +2. **publish** — uploads the artifacts to PyPI via OIDC. +3. **github-release** — creates a [GitHub Release](https://github.com/cloudglue/cloudglue-python/releases) + for the tag with auto-generated notes and the `.tar.gz` + `.whl` attached. + +#### One-time setup + +This is already configured for the `cloudglue` project, but for reference the +workflow depends on: + +- A [PyPI trusted publisher](https://pypi.org/manage/project/cloudglue/settings/publishing/) + for repo `cloudglue/cloudglue-python`, workflow `publish.yml`, environment `pypi`. +- A GitHub Environment named `pypi` (repo **Settings → Environments**). + +#### Manual publishing (fallback) + +The legacy token-based path still works if you need to publish outside CI. It +requires a [PyPI API token](https://pypi.org/manage/account/token/) scoped to the +`cloudglue` project, in `~/.pypirc` or via env vars: + +```bash +make build +TWINE_USERNAME=__token__ TWINE_PASSWORD='pypi-...' make publish +``` + ### Project Structure Project directory structure described below: From 38c054f6fdfc671183a64cc053fc35d9ebf4864e Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 14:00:52 -0700 Subject: [PATCH 4/8] ci: disable credential persistence on checkout Set persist-credentials: false on actions/checkout in the build job so the GITHUB_TOKEN is not left in git config while python -m build executes third-party build-backend code (zizmor artipacked finding from CodeRabbit). --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e00a2b74..c704bffe 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false # don't leave GITHUB_TOKEN in git config; build runs third-party code # Submodule (spec/) is not needed — cloudglue/sdk is already committed. - uses: actions/setup-python@v5 From 5b6e8e39f159fc04c58023d713663cd96dba4caf Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 14:06:36 -0700 Subject: [PATCH 5/8] ci: add actions:read to publish and github-release jobs These jobs declare job-level permissions, which sets all unlisted scopes to none. Add actions:read so actions/download-artifact can reliably fetch the build artifact (defensive; flagged by Cursor Bugbot). --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c704bffe..5169a9e1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -60,6 +60,7 @@ jobs: url: https://pypi.org/p/cloudglue permissions: id-token: write # required for OIDC trusted publishing + actions: read # allow download-artifact to fetch the build artifact steps: - uses: actions/download-artifact@v4 with: @@ -74,6 +75,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write # required to create the release and upload assets + actions: read # allow download-artifact to fetch the build artifact steps: - uses: actions/download-artifact@v4 with: From d25ca876d982867e9ac539e4a548dbcf3b732229 Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 14:10:50 -0700 Subject: [PATCH 6/8] ci: declare explicit minimal permissions on build job Add permissions: contents: read to the build job so it no longer inherits the broad repository-default GITHUB_TOKEN scope (least-privilege). This is sufficient: checkout needs contents:read, and upload-artifact uploads the same-run artifact via the Actions runtime token, not GITHUB_TOKEN, so no actions:write is required (Cursor Bugbot finding). --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5169a9e1..1a255e54 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,8 @@ jobs: build: name: Build distribution runs-on: ubuntu-latest + permissions: + contents: read # for checkout; upload-artifact uses the Actions runtime token, not GITHUB_TOKEN steps: - uses: actions/checkout@v4 with: From d4f76ea61b6303e18dbf805bdc38ebc8de04d220 Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 14:18:29 -0700 Subject: [PATCH 7/8] pr-review: reconcile release workflow after bot rounds Holistic drift review (high-effort /code-review) findings: - Header quickstart used v0.7.15 (already published/tagged) so it could not trigger a release; bump example to v0.7.16 to match the README. - Add skip-existing: true to pypa/gh-action-pypi-publish so re-running after a successful upload is idempotent instead of failing on a duplicate version. - Remove actions: read from the publish and github-release jobs: same-run download-artifact uses the Actions runtime token, not GITHUB_TOKEN, so the scope was unnecessary and its comment was misleading. Restores least-privilege matching the canonical PyPA template (reverts an over-broad bot-round fix). - Drop >&2 on the ::error:: line so GitHub renders it as an annotation (workflow commands are parsed from stdout). --- .github/workflows/publish.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1a255e54..eb2ec946 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,8 +3,8 @@ name: Publish to PyPI # Publishes to PyPI via OIDC trusted publishing whenever a v* tag is pushed. # No API tokens required — PyPI verifies the GitHub OIDC identity instead. # -# git tag v0.7.15 -# git push origin v0.7.15 +# git tag v0.7.16 # must match the version in pyproject.toml +# git push origin v0.7.16 on: push: @@ -35,7 +35,7 @@ jobs: PKG_VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')" TAG_VERSION="${GITHUB_REF_NAME#v}" if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then - echo "::error::Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match pyproject version $PKG_VERSION" >&2 + echo "::error::Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match pyproject version $PKG_VERSION" exit 1 fi echo "Building and publishing version $PKG_VERSION" @@ -62,7 +62,6 @@ jobs: url: https://pypi.org/p/cloudglue permissions: id-token: write # required for OIDC trusted publishing - actions: read # allow download-artifact to fetch the build artifact steps: - uses: actions/download-artifact@v4 with: @@ -70,6 +69,8 @@ jobs: path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true # idempotent: re-running after a successful upload won't fail on a duplicate github-release: name: Create GitHub Release @@ -77,7 +78,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: write # required to create the release and upload assets - actions: read # allow download-artifact to fetch the build artifact steps: - uses: actions/download-artifact@v4 with: From 1c37a57b12589d712ea2ced6d8c8cde4ae6a80e7 Mon Sep 17 00:00:00 2001 From: KDR Date: Tue, 30 Jun 2026 14:24:06 -0700 Subject: [PATCH 8/8] ci: drop skip-existing to keep a clear publish invariant Reconciles two conflicting review findings. The earlier code-review added skip-existing: true for idempotent re-runs, but Bugbot correctly noted it can mask a no-op publish and let github-release attach artifacts that don't match PyPI for that version. The common recovery case (github-release flaking after a successful publish) is already covered by GitHub's 're-run failed jobs', which doesn't re-run the green publish job. skip-existing only helps the rarer 're-run all jobs' path, which is exactly where it creates the mismatch. Removing it preserves the invariant: a green publish job means these exact artifacts reached PyPI. --- .github/workflows/publish.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eb2ec946..6219f0a6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -69,8 +69,6 @@ jobs: path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 - with: - skip-existing: true # idempotent: re-running after a successful upload won't fail on a duplicate github-release: name: Create GitHub Release