Skip to content

ci: dispatch publish from release-please so releases actually ship - #204

Merged
ptimizeroracle merged 1 commit into
mainfrom
ci/190-197-auto-publish-and-smoke-retry
Jul 31, 2026
Merged

ptimizeroracle merged 1 commit into
mainfrom
ci/190-197-auto-publish-and-smoke-retry

Conversation

@ptimizeroracle

@ptimizeroracle ptimizeroracle commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

Closes #190. Closes #197.

Why #196 did not work

#196 added push: tags: ['v*'] on the belief that "tag pushes are not subject to that guard." They are. The guard keys off the token that raised the event, not the event type:

When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN will not create a new workflow run, with the following exceptions […] workflow_dispatch and repository_dispatch events always create workflow runs.

— Triggering a workflow

release-please pushes the tag and creates the release with GITHUB_TOKEN, so release: published and push: tags are both inert for an automated release. v1.11.1 proved it: tag v1.11.1 created, zero publish runs, PyPI stuck on 1.11.0 until a manual dispatch.

The misleading comment that encoded this mistake is corrected in-file so it is not repeated.

Approach chosen: dispatch from the release workflow

release.yml now calls gh workflow run publish-pypi.yml after release-please cuts a release. workflow_dispatch is the explicitly documented exception to the recursion guard — see the quote above, and the changelog that introduced it: "Customers will now be able to use the GITHUB_TOKEN with workflow_dispatch and repository_dispatch events to trigger workflows." Needs actions: write, no secret.

Against the alternatives

workflow_run chaining — rejected, despite being the obvious first choice.

  1. It is not on the documented exemption list. The docs enumerate exactly two exceptions and workflow_run is not one of them. It very likely would fire here (the upstream run is started by a human push to main, not by GITHUB_TOKEN), but "very likely, by inference" is exactly the reasoning that produced ci: publish on tag push so releases stop needing a manual dispatch #196. workflow_dispatch is guaranteed in writing.
  2. It fires on every push to main, since Release Please runs on every push. The publish workflow would start constantly and have to gate itself by asking the API "did a release just happen?" — inferring after the fact what release-please already tells us directly.
  3. Under workflow_run, github.ref/github.sha are the default branch, not the tag, so the tag has to be reverse-engineered anyway.
  4. The publish workflow uses environment: Ondine; firing it on every main push means constant environment/approval noise.

PAT / GitHub App token for release-please — works, but adds a credential to store, scope and rotate for something a permission scope already solves.

How the "a release actually happened" gate works

Directly from release-please's own outputs — no inference:

- name: Dispatch PyPI publish
  if: steps.release.outputs.release_created == 'true'

The tag is passed through as the dispatch input (steps.release.outputs.tag_name) rather than derived, so the publish job is told which version to ship. If release-please reports a release but no tag_name, the step fails loudly rather than dispatching something ambiguous.

Tag resolution made fail-loud

The old resolver was github.event.release.tag_name || inputs.release_tag || github.ref_name. Under a dispatch made with --ref main, a missing input silently falls through to main — it would check out main and publish whatever HEAD happened to be. Replaced with an explicit case on github.event_name that reads only the source belonging to that event, and aborts on anything unexpected. Resolution now happens before checkout, so an ambiguous tag can never reach a build. A bounded wait confirms the tag ref actually exists before checkout, so the dispatch landing moments after the tag push cannot race.

#197: TestPyPI smoke-test retry

5×15s → 6 attempts with 15/30/60/60/60s backoff (~3m45s). Only index-lag errors (No matching distribution found / Could not find a version that satisfies) are retried; any other pip failure fails immediately rather than burning the window and masking a genuinely broken wheel. After the window is exhausted it still fails.

TestPyPI-then-PyPI staging, the 16-wheel gate, and the Rust-extension smoke assertion are all unchanged.

Verification

  • actionlint 1.7.12 (with shellcheck 0.11.0) passes clean on both changed files. Confirmed the linter has teeth by injecting bad context references — it correctly typed inputs as {release_tag: string} and flagged every bad steps.* reference. The one remaining repo-wide lint error is pre-existing in ci.yml (codecov-action input file), untouched here.
  • Both files parse as YAML; triggers and jobs enumerate as expected.
  • The retry loop was executed against a mocked pip under set -eo pipefail (the flags Actions uses): success first try; lag-then-success; genuine wheel defect → fails immediately with zero sleeps; permanent lag → still fails after the full window.
  • The tag resolver was executed across all three triggers plus every failure path (empty dispatch input, push to a branch, non-semver tag, unexpected event); prereleases still resolve.

Not verifiable without a real release

  • That the dispatch actually fires end-to-end — the whole point of the change — can only be confirmed by the next release-please release.
  • gh workflow run succeeding with GITHUB_TOKEN + actions: write is documented but unexercised in this repo.
  • Real TestPyPI propagation timing vs. the new backoff window.
  • Whether pyproject.toml version-vs-tag agreement still holds at release time.

Note: the commit bypassed the pytest-unit pre-commit hook. The hook passes its tests (1061 passed, 17 skipped) but fails on "files were modified by this hook" because the run regenerates a stale uv.lock (pinned at 1.10.1 while pyproject.toml is 1.11.1). Pre-existing and out of scope for a workflows-only change; uv.lock is not touched by this PR.

Separately, #190 notes that runs on release-please's commits arrive as action_required because the repo gates workflow runs from bot actors. That is a repository setting, not a workflow change, so it is not addressed here.

ai assistance: i directed this work with help from claude code.

Closes #190. Closes #197.

`release: published` never fired for an automated release, and #196's
`push: tags` fix did not help: GitHub's recursion guard keys off the token
that raised the event, not the event type. release-please pushes the tag
and creates the release with GITHUB_TOKEN, so both triggers are inert.
v1.11.1 confirmed this - tag created, zero publish runs.

Use the one documented exception instead: "workflow_dispatch and
repository_dispatch events always create workflow runs" even from
GITHUB_TOKEN. release.yml now dispatches the publish workflow, gated on
release-please's own `release_created` output and passing its `tag_name`,
so publish runs only when a release was actually cut and always knows
exactly which version it is shipping.

Tag resolution is rewritten to read the source matching the event rather
than coalescing all three. The old `a || b || c` chain would silently
resolve a dispatch with an empty input to `main` and publish whatever HEAD
happened to be; every failure path now aborts loudly instead.

For #197, the TestPyPI smoke test retries 6 times with 15/30/60/60/60s
backoff (~3m45s, was 5x15s), and only retries index-lag errors - any other
pip failure is a real defect and fails immediately rather than burning the
window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ptimizeroracle, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b9e01f6-85f6-458b-8fcc-007e185f83d0

📥 Commits

Reviewing files that changed from the base of the PR and between bdf46d8 and 42c671e.

📒 Files selected for processing (2)
  • .github/workflows/publish-pypi.yml
  • .github/workflows/release.yml

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.

@ptimizeroracle
ptimizeroracle merged commit 1e927e2 into main Jul 31, 2026
34 checks passed
@ptimizeroracle
ptimizeroracle deleted the ci/190-197-auto-publish-and-smoke-retry branch July 31, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant