Skip to content

ci: consolidate release into one dispatch-driven workflow#83

Open
yaseenisolated wants to merge 6 commits into
mainfrom
ci/consolidate-release
Open

ci: consolidate release into one dispatch-driven workflow#83
yaseenisolated wants to merge 6 commits into
mainfrom
ci/consolidate-release

Conversation

@yaseenisolated

@yaseenisolated yaseenisolated commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Why

v2.5.2 shipped to PyPI with no binaries, breaking pip install cerebriumcerebrium <cmd> with a 404 for every user. Root cause was the event-chained release design:

  • tag-and-release.yml → (tag-push event) → deploy-release.yml → (release-published event) → pypi-publish.yml.
  • A tag pushed with GITHUB_TOKEN does not trigger downstream workflows, so GoReleaser silently never ran — but the PyPI wrapper (a thin launcher that downloads those binaries at runtime) shipped anyway.
  • Event chaining also hid where a release stalled — every link is invisible when it doesn't fire.

What

Add a single Release orchestrator (workflow_dispatch only) that runs the whole pipeline in one visible run, ordered by needs: — nothing triggers anything else via events:

validate → test → tag → goreleaser → verify-assets → wrapper-smoke → pypi
                                                                       └ cleanup (on failure)
  • No silent hand-off — the tag is created as a pipeline step, so the GITHUB_TOKEN-tag bug can't exist.
  • Every job runs on the resolved release commit/tag → GoReleaser's git-state check can't be tripped by a dispatch running off a branch (the other half of the v2.5.2 failure).
  • PyPI is gated and lastverify-assets (all binaries + checksums present) and wrapper-smoke (the wrapper actually runs against the real release) both pass before the PyPI publish, which can't be un-published.
  • cleanup deletes the tag + release on any failure for clean re-runs.
  • Fixes the old "Test installation" step whose unconditional exit 0 swallowed the binary-download failure (it logged the 404 and went green).

Files (minimal churn — only one new file, no renames of existing workflows)

  • release.yml (new) — the orchestrator. Net-new; there was no orchestrator before.
  • deploy-release.yml — same filename, reduced to the reusable (workflow_call) GoReleaser build; the test/smoke jobs moved into the orchestrator.
  • pypi-publish.yml — same filename (keeps the PyPI trusted-publisher binding valid); trigger swapped from release: published to workflow_call, plus the test-step fix and a binaries-exist guard.
  • test.yml — now also workflow_call-able, so the orchestrator runs the same matrix that gates PRs.
  • Deleted tag-and-release.yml (its tag creation is now the orchestrator's tag job).
  • Docs (RELEASING.md, PYPI_RELEASE_STRATEGY.md) + version-bump comments updated to the dispatch flow.

actionlint clean across all workflows.

⚠️ Verify before merge

  • PyPI trusted publisher — publishing uses OIDC (no token). Filename unchanged (pypi-publish.yml), so the existing entry should keep working — just confirm it points at pypi-publish.yml.
  • Branch protection / required checks — if Deploy Release / Tag were required status checks, those names no longer exist as standalone triggerable workflows.
  • Dry-run the full pipeline against a throwaway prerelease before trusting it:
    gh workflow run release.yml --ref ci/consolidate-release -f version=v0.0.1-rc.1
    # tear down: gh release delete v0.0.1-rc.1 --cleanup-tag

How to release after this merges

gh workflow run release.yml -f version=v2.5.3

🤖 Generated with Claude Code

yaseenisolated and others added 6 commits June 10, 2026 14:33
Replace the three event-chained release workflows (tag-and-release ->
deploy-release -> pypi-publish) with a single `Release` orchestrator that
runs the whole pipeline in one workflow_dispatch run, ordered by `needs:`:

    validate -> test -> tag -> goreleaser -> verify-assets
                                              -> wrapper-smoke -> pypi
                                              -> cleanup (on failure)

Why: the old flow chained workflows via tag-push / release-published
events. A tag pushed with GITHUB_TOKEN does not trigger downstream
workflows, so the build silently never ran and the PyPI wrapper shipped
pointing at binaries that did not exist -> `pip install` then 404'd for
every user. Event chaining also hid where a release stalled.

This design removes the chaining entirely:
- One run, visible end to end. No silent hand-offs.
- Tag is created as a pipeline step (no PAT-trigger hack needed).
- Every job operates on the resolved release commit/tag, so GoReleaser's
  git-state check can't be tripped by a dispatch running off a branch.
- verify-assets + wrapper-smoke gate PyPI on the binaries actually
  existing and the wrapper actually running against the release; PyPI
  (which can't be un-published) is the last step.
- cleanup deletes the tag + release on failure for clean re-runs.

Reusable sub-workflows (workflow_call): test.yml (shared with PR CI),
release-goreleaser.yml, release-pypi.yml. release-pypi.yml also fixes the
old "Test installation" step whose unconditional `exit 0` swallowed the
binary-download failure.

Docs (RELEASING.md, PYPI_RELEASE_STRATEGY.md) and the version-bump
comments updated to the dispatch flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reuse the existing pypi-publish.yml filename (swap its trigger to
workflow_call + apply the fixes) rather than renaming to release-pypi.yml.
PyPI trusted publishing binds to the workflow filename, so keeping the
name avoids reconfiguring the trusted publisher on PyPI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avoid the needless rename to release-goreleaser.yml — reuse the existing
deploy-release.yml filename for the (now workflow_call-only) GoReleaser
build. The only genuinely new file is the orchestrator (release.yml);
no existing workflow is renamed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the Release workflow's `version` input optional. When left blank,
validate resolves the latest stable vX.Y.Z tag and bumps the patch
(e.g. v2.5.2 -> v2.5.3). Minor/major bumps still require an explicit
version. Errors if no version is given and no stable tag exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GitHub release is now created as a prerelease (.goreleaser.yaml
  prerelease: true). A new `promote` job flips a stable release to
  "Latest" only after verify-assets + wrapper-smoke + pypi all pass.
  Actual prerelease tags (-rc/-beta) are never promoted.

- Add a `dry-run` input to the Release workflow: still pushes the tag
  and creates a real GitHub prerelease with binaries, and runs the
  verification jobs, but never promotes, never publishes to PyPI, and
  skips the Homebrew tap push + macOS notarization. For testing the
  pipeline end to end without affecting stable users.

- Homebrew skip_upload is now keyed explicitly off the tag's semver
  prerelease component (not release.prerelease, which is forced true),
  so stable releases still update the tap.

- cleanup now only tears down when the build itself fails; once
  goreleaser succeeds the prerelease is valid and is left in place on a
  later failure (avoids dangling a pushed Homebrew cask).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
workflow_call:
inputs:
tag:
description: "Release tag (e.g. v2.5.2 or 2.5.2)."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the version should always have the v prefix

Suggested change
description: "Release tag (e.g. v2.5.2 or 2.5.2)."
description: "Release tag (e.g. v2.5.2)."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nvm, I see it sorts it out below

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