ci: tag-based PyPI publishing via OIDC + GitHub Releases + docs#67
Conversation
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
📝 WalkthroughWalkthroughAdded a new GitHub Actions workflow file that triggers on pushed version tags, validates the tag matches the package version in pyproject.toml, builds and checks distribution artifacts, then publishes them to PyPI using OIDC trusted publishing across two dependent jobs. ChangesPyPI Publish Workflow
Sequence Diagram(s)sequenceDiagram
participant Tag as Git Tag Push
participant Build as Build Job
participant Publish as Publish Job
participant PyPI
Tag->>Build: push tag matching v*
Build->>Build: validate tag == pyproject.toml version
Build->>Build: build sdist/wheel, run twine check
Build->>Build: upload dist artifact
Publish->>Build: download dist artifact
Publish->>PyPI: publish via OIDC trusted publishing
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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
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.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/publish.yml (1)
1-68: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winAdd an explicit top-level
permissionsblock.No
permissions:is set for the workflow or thebuildjob, so the default (potentially broad, org-dependent)GITHUB_TOKENscope is used for a job that runs unreviewed build tooling. Restrict to least privilege.🔒️ Proposed fix
on: push: tags: - "v*" +permissions: + contents: read + jobs: build:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/publish.yml around lines 1 - 68, Add an explicit top-level permissions block in the publish workflow to follow least privilege for the default GITHUB_TOKEN used by the build job. Update the workflow so the build-related steps in the build job only get the minimum access they need, while keeping the publish job’s id-token: write for pypa/gh-action-pypi-publish. Use the workflow’s top-level settings and the build/publish job sections to place the restriction without changing the artifact build or upload/download flow.Source: Linters/SAST tools
🧹 Nitpick comments (1)
.github/workflows/publish.yml (1)
19-19: 🔒 Security & Privacy | 🔵 Trivial | ⚖️ Poor tradeoffConsider pinning third-party actions to a commit SHA.
Actions are referenced by mutable tags (
@v4,@release/v1). For a workflow that ultimately holds OIDC publish permission to PyPI, pinning to a commit SHA reduces supply-chain risk from a compromised/retagged release.Also applies to: 22-22, 47-47, 62-62, 67-67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/publish.yml at line 19, The publish workflow uses mutable third-party action tags, so pin each referenced action to an immutable commit SHA instead of version tags. Update the action references in the workflow (including actions/checkout and the other third-party actions called out in this file) to fixed SHAs so the publish job cannot be altered by a retagged release.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/publish.yml:
- Around line 19-20: The checkout step in the publish workflow should disable
persisted Git credentials to avoid leaving the GITHUB_TOKEN in local git config
during the rest of the job. Update the actions/checkout usage in the workflow to
set persist-credentials to false, keeping the change scoped to the checkout step
that precedes the build process.
---
Outside diff comments:
In @.github/workflows/publish.yml:
- Around line 1-68: Add an explicit top-level permissions block in the publish
workflow to follow least privilege for the default GITHUB_TOKEN used by the
build job. Update the workflow so the build-related steps in the build job only
get the minimum access they need, while keeping the publish job’s id-token:
write for pypa/gh-action-pypi-publish. Use the workflow’s top-level settings and
the build/publish job sections to place the restriction without changing the
artifact build or upload/download flow.
---
Nitpick comments:
In @.github/workflows/publish.yml:
- Line 19: The publish workflow uses mutable third-party action tags, so pin
each referenced action to an immutable commit SHA instead of version tags.
Update the action references in the workflow (including actions/checkout and the
other third-party actions called out in this file) to fixed SHAs so the publish
job cannot be altered by a retagged release.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 44150693-41b7-4461-8eb7-d9eacb5376fa
📒 Files selected for processing (1)
.github/workflows/publish.yml
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).
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).
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).
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).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d4f76ea. Configure here.
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.

Summary
.github/workflows/publish.yml: on everyv*tag push, build the package, verify the tag matchespyproject.toml, publish to PyPI via OIDC trusted publishing (no stored tokens), then create a GitHub Release with the.tar.gz+.whlattached.make publishfallback) in the README Releasing section.persist-credentials: false.Test plan
Validated locally on this branch (commit
1c37a57):actionlint1.7.12 — clean, no findings (workflow syntax, expression checks, action input schemas, and shellcheck on therun:blocks).Verify tag matches pyproject versionstep: matching tagv0.7.15→ exit 0 (Building and publishing version 0.7.15); mismatching tagv0.7.16→ exit 1 with the::error::line emitted on stdout (so GitHub renders it as an annotation, not a plain log line).python -m buildproducescloudglue-0.7.15.tar.gz+cloudglue-0.7.15-py3-none-any.whl;twine check dist/*→ both PASSED.build → publish → github-release, triggerpush: tags: [v*], minimal per-job permissions (buildcontents:read, publishid-token:write+pypienvironment, github-releasecontents:write).pypienvironment — created and restricted tov*tag deployments only.Requires a live
v*tag push (first real release, e.g.v0.7.16) — cannot be verified pre-merge:cloudglue/cloudglue-python, workflowpublish.yml, environmentpypi).publishuploads to PyPI via OIDC with no token prompt..tar.gz+.whlattached.Why
make publishrequires a long-lived PyPI API token (opaque403s when it expires/rotates). Trusted publishing has PyPI verify the GitHub OIDC identity of the run instead, so there's no secret to manage or leak. Attaching dists to a GitHub Release gives a permanent record independent of PyPI.Jobs
build— checkout (persist-credentials: false; submodule intentionally not fetched sincecloudglue/sdkis committed), verify tag matchespyproject.tomlversion (fails fast on mismatch), build sdist + wheel, runtwine check, upload artifact.publish— download artifact and publish withpypa/gh-action-pypi-publishusingid-token: writeand apypienvironment.github-release— runs only after a successful publish; creates a GitHub Release with auto-generated notes and the.tar.gz+.whlattached (contents: writescoped to this job).Required one-time setup
cloudglue, repocloudglue-python, workflowpublish.yml, environmentpypi.pypi.Release flow afterwards
# bump version in pyproject.toml, commit, then: git tag v0.7.16 git push origin v0.7.16The legacy
make publishtoken path still works as a manual fallback.Note
Medium Risk
Changes how production packages reach PyPI and require correct trusted-publisher and environment setup; a misconfigured or bypassed tag/version check could publish the wrong version.
Overview
Adds automated release CI triggered by
v*tags: build verifies the tag matchespyproject.toml, produces and checks dist artifacts, publishes to PyPI with OIDC trusted publishing (no stored tokens), then creates a GitHub Release with the sdist and wheel attached.The README gains a Releasing section (tag flow, one-time PyPI/GitHub
pypienvironment setup, manualmake publishfallback). The workflow checkout usespersist-credentials: falseand skips the spec submodule because the SDK is already committed.Reviewed by Cursor Bugbot for commit 1c37a57. Bugbot is set up for automated code reviews on this repo. Configure here.