Skip to content

ci: don't lose the GitHub Release when the PyPI upload fails - #50

Merged
blackaxgit merged 1 commit into
mainfrom
ci/decouple-github-release
Jul 26, 2026
Merged

ci: don't lose the GitHub Release when the PyPI upload fails#50
blackaxgit merged 1 commit into
mainfrom
ci/decouple-github-release

Conversation

@blackaxgit

Copy link
Copy Markdown
Owner

What happened (v0.6.0 release incident, 2026-07-26)

github-release had needs: [publish-pypi] and no if:, so it inherited the default "skip if any dependency failed".

The PyPI trusted-publisher exchange failed with invalid-publisher — a registry-side configuration problem that uploads nothing:

* `invalid-publisher`: valid token, but no corresponding publisher
  sub: repo:blackaxgit/ssh-mcp:environment:pypi

Consequences: the tag was pushed and the container image published and attested, but the GitHub Release was skipped and its artifacts (wheel, sdist, CycloneDX SBOM) went with it. Re-running could not recover it, because a tag's workflow is pinned at the tag ref — so v0.6.0's release had to be created by hand from the run's artifacts.

Fix

A GitHub Release documents the tag. PyPI is an independent distribution channel with its own failure modes (registry outage, publisher config, version already present). One should not erase the other.

needs: [build, publish-pypi]
if: always() && needs.build.result == 'success'
  • always() so a failed publish-pypi no longer skips it.
  • explicit needs.build.result == 'success' because always() alone would also run the job after a failed build, when there is no artifact to attach — it would fail confusingly instead of being skipped.
  • publish-pypi stays in needs: for ordering only, so the release is still created after the upload attempt rather than racing it.

Verification

  • New regression test test_github_release_survives_a_failed_pypi_publishfails on main's release.yml (assert 'build' in ['publish-pypi']) and passes here.
  • pytest718 passed
  • ruff format --check / ruff check src/ tests/ / mypy src/ — clean
  • gitleaks scoped to origin/main..HEAD — no leaks

Still requires operator action (cannot be done from repo files)

  1. Configure the PyPI trusted publisher — this is the actual release blocker. Owner blackaxgit, repository ssh-mcp, workflow release.yml, environment pypi. PyPI's latest is still 0.1.4; 0.6.0 was never uploaded, so the version is still free.
  2. Add required reviewers to the pypi environment. It was auto-created by the first run with no protection rules, so the reviewer gate that AGENTS.md and CHANGELOG.md both describe does not currently exist.

Release incident 2026-07-26 (v0.6.0): github-release had needs: [publish-pypi]
and no if:, so a PyPI-side trusted-publisher misconfiguration — which fails the
OIDC token exchange before any upload — skipped the GitHub Release too. The tag
and the container image were published, but the release record and its
artifacts were lost, and re-running could not recover them because a tag's
workflow is pinned at the tag ref. v0.6.0's release had to be created by hand.

A GitHub Release documents the tag; PyPI is an independent channel with its own
failure modes. github-release now gates on build and runs even when
publish-pypi failed, with an explicit needs.build.result check so it is still
skipped when there is no artifact to attach. publish-pypi stays in needs: for
ordering only.
@blackaxgit
blackaxgit merged commit 2236572 into main Jul 26, 2026
7 checks passed
@blackaxgit
blackaxgit deleted the ci/decouple-github-release branch July 26, 2026 23:56
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.

1 participant