Skip to content

ci: tag-based PyPI publishing via OIDC + GitHub Releases + docs#67

Merged
kdr merged 8 commits into
mainfrom
ci/oidc-pypi-publish
Jun 30, 2026
Merged

ci: tag-based PyPI publishing via OIDC + GitHub Releases + docs#67
kdr merged 8 commits into
mainfrom
ci/oidc-pypi-publish

Conversation

@kdr

@kdr kdr commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds .github/workflows/publish.yml: on every v* tag push, build the package, verify the tag matches pyproject.toml, publish to PyPI via OIDC trusted publishing (no stored tokens), then create a GitHub Release with the .tar.gz + .whl attached.
  • Documents the tag-based release flow (and the manual make publish fallback) in the README Releasing section.
  • Hardens the build checkout with persist-credentials: false.

Test plan

Validated locally on this branch (commit 1c37a57):

  • actionlint 1.7.12 — clean, no findings (workflow syntax, expression checks, action input schemas, and shellcheck on the run: blocks).
  • Version gate — replicated the exact Verify tag matches pyproject version step: matching tag v0.7.15 → exit 0 (Building and publishing version 0.7.15); mismatching tag v0.7.16 → exit 1 with the ::error:: line emitted on stdout (so GitHub renders it as an annotation, not a plain log line).
  • Build job stepspython -m build produces cloudglue-0.7.15.tar.gz + cloudglue-0.7.15-py3-none-any.whl; twine check dist/* → both PASSED.
  • Workflow structure — 3 jobs chained build → publish → github-release, trigger push: tags: [v*], minimal per-job permissions (build contents:read, publish id-token:write + pypi environment, github-release contents:write).
  • GitHub pypi environment — created and restricted to v* tag deployments only.

Requires a live v* tag push (first real release, e.g. v0.7.16) — cannot be verified pre-merge:

  • PyPI trusted publisher configured (cloudglue/cloudglue-python, workflow publish.yml, environment pypi).
  • publish uploads to PyPI via OIDC with no token prompt.
  • GitHub Release created for the tag with .tar.gz + .whl attached.

Note: 0.7.15 is already published on PyPI (immutable), so the first end-to-end run will be the next version bump (0.7.16). A failed OIDC handshake does not consume the version, so the first live release doubles safely as the end-to-end test.

Why

make publish requires a long-lived PyPI API token (opaque 403s 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 since cloudglue/sdk is committed), verify tag matches pyproject.toml version (fails fast on mismatch), build sdist + wheel, run twine check, upload artifact.
  • publish — download artifact and publish with pypa/gh-action-pypi-publish using id-token: write and a pypi environment.
  • github-release — runs only after a successful publish; creates a GitHub Release with auto-generated notes and the .tar.gz + .whl attached (contents: write scoped to this job).

Required one-time setup

  1. PyPIhttps://pypi.org/manage/project/cloudglue/settings/publishing/ → add a GitHub trusted publisher: owner cloudglue, repo cloudglue-python, workflow publish.yml, environment pypi.
  2. GitHub → Settings → Environments → create pypi.

Release flow afterwards

# bump version in pyproject.toml, commit, then:
git tag v0.7.16
git push origin v0.7.16

The legacy make publish token 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 matches pyproject.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 pypi environment setup, manual make publish fallback). The workflow checkout uses persist-credentials: false and 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.

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
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added 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.

Changes

PyPI Publish Workflow

Layer / File(s) Summary
Trigger and build pipeline
.github/workflows/publish.yml
Workflow triggers on v* tag pushes, checks out code, sets up Python 3.12, validates that the tag version matches pyproject.toml's project version, builds sdist/wheel, runs twine check, and uploads dist/ as an artifact.
Publish job
.github/workflows/publish.yml
A dependent publish job downloads the built dist/ artifact and publishes it to PyPI via pypa/gh-action-pypi-publish, using the pypi environment and id-token: write for OIDC trusted publishing.

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
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A tag goes hop, a build takes flight,
twine checks the parcel, snug and tight,
🐇 through OIDC clouds it soars,
no secret keys behind closed doors,
to PyPI shelves, the carrot's spied,
released at last with a happy stride!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: adding tag-based PyPI publishing with OIDC and related release docs.
Description check ✅ Passed The description follows the required Summary and Test plan template and provides detailed, relevant release and validation information.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/oidc-pypi-publish

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

kdr added 2 commits June 30, 2026 13:57
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add an explicit top-level permissions block.

No permissions: is set for the workflow or the build job, so the default (potentially broad, org-dependent) GITHUB_TOKEN scope 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 tradeoff

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 61ac405 and b713e50.

📒 Files selected for processing (1)
  • .github/workflows/publish.yml

Comment thread .github/workflows/publish.yml
@kdr kdr changed the title ci: publish to PyPI via OIDC trusted publishing on v* tags ci: tag-based PyPI publishing via OIDC + GitHub Releases + docs Jun 30, 2026
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).
@kdr
kdr requested a review from amyxst June 30, 2026 21:03
Comment thread .github/workflows/publish.yml
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).
Comment thread .github/workflows/publish.yml
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).

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread .github/workflows/publish.yml
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.
@kdr
kdr merged commit 9d8d2b4 into main Jun 30, 2026
2 checks passed
@kdr
kdr deleted the ci/oidc-pypi-publish branch June 30, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants