fix(release): never attempt a PyPI upload from workflow_dispatch - #52
Conversation
`release.yml` accepts `workflow_dispatch`, but `publish` was only gated on `is-prerelease`. A manual run therefore proceeded all the way to the upload and would always fail, because PyPI rejects re-uploading an existing version. - `publish`, `create-github-release` and `post-release-validation` are now additionally gated on `startsWith(github.ref, 'refs/tags/')`. A manual dispatch becomes a dry run of build + quality gates with no upload attempt and no "GitHub release missing" failure at the end. - `pre-release.yml` is dispatch-only by design (it generates the rc/alpha/beta version itself), so a tag guard would disable it outright and is deliberately not applied. Its workflow-level `id-token: write` / `contents: write` / `packages: write` is rescoped instead: the default is now `contents: read`, `id-token: write` lives only in `test-pypi-upload` and `contents: write` only in `create-prerelease`. Unused `packages: write` removed. Matches the guard already present in trsdn/paperless-mcp and trsdn/obsidian-mcp. The post-publish job fixes previously in this branch landed via #50; this branch was rebuilt on top of main to keep only the remaining change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
🔍 PR Quality SummaryCI Status🔄 Workflows in progress... Metrics
Quality Checks
MCP Tools
🤖 Auto-generated by CI • Last updated: 2026-08-24 22:33 UTC |
|
Die Permissions-Härtung in Der Guard auf if: |
startsWith(github.ref, 'refs/tags/')
&& needs.validate-release.outputs.is-prerelease == 'false'Warum das den dokumentierten Re-Run-Pfad abschaltetDer Kommentar begründet den Guard damit, ein workflow_dispatch:
inputs:
tag:
description: 'Existing release tag to (re-)run the pipeline for, e.g. v1.2.3'
required: true
type: stringDer Input ist required, und jeder Job checkt konsequent Bei Das ist kein theoretischer Fallv2.0.0 hat drei Anläufe gebraucht ( Das Szenario im Kommentar ist bereits abgedecktDie Sorge vor einem doppelten Upload ist berechtigt, aber - name: Check if tag already exists in releases
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "❌ Release for tag $TAG already exists"
exit 1
fiEin Re-Run für einen vollständig abgeschlossenen Release bricht damit früh und laut ab. Ein Re-Run für einen halb fehlgeschlagenen Release ist genau der Fall, den man will. Vorschlag: den Zur Einordnung, weil es leicht zu verwechseln istIn trsdn/obsidian-mcp habe ich denselben Guard bewusst eingebaut (PR #11) — dort hat |
…eline (#54) PR #52 added `startsWith(github.ref, 'refs/tags/')` to publish, create-github-release and post-release-validation. That guard is wrong for this workflow: workflow_dispatch takes a *required* `tag` input and every job checks it out via `inputs.tag || github.ref`, so a dispatch is the documented way to re-run the pipeline for an existing tag. On a dispatch `github.ref` is `refs/heads/main`, so the guard was always false. The upload was skipped silently and the run still went green -- failure that looks like success. v2.0.0 needed three attempts, which is exactly when that re-run path matters. Duplicate uploads were never the risk this guard protected against: validate-release already exits 1 via "Check if tag already exists in releases". The least-privilege permissions work from #52 is kept untouched. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Follow-up to #48 / #50. Last remaining hardening:
release.ymlacceptsworkflow_dispatch, but nothing stopped a manual run from attempting a PyPI upload.publishwas gated only onis-prerelease, so a manual dispatch ran all the way to the upload and would always fail — PyPI rejects re-uploading an existing version.Fix
startsWith(github.ref, 'refs/tags/')added to three jobs:publishstartsWith(github.ref, 'refs/tags/') && needs.validate-release.outputs.is-prerelease == 'false'create-github-releasealways() && startsWith(github.ref, 'refs/tags/') && …post-release-validationalways() && startsWith(github.ref, 'refs/tags/')post-release-validationneeded it too: with a bareif: always()every dispatch would end in "GitHub release missing". A manual dispatch is now a clean dry run of build + quality gates.This matches the guard already in place in
trsdn/paperless-mcpandtrsdn/obsidian-mcp.pre-release.yml: tag guard deliberately NOT appliedThat workflow is triggered only by
workflow_dispatchand generates the rc/alpha/beta version itself, so a tag guard would disable it entirely. The TestPyPI duplicate risk there only materialises if the same version is generated twice — a separate question that should be decided on its own.What was fixed there instead is the same least-privilege gap already closed in
release.yml—id-token: writesat at workflow level:packages: writewas unused and is removed.Validation
actionlinton both workflows: 12 findings, identical tomain(all pre-existing, in untouched lines).ifexpressions and the rescoped permissions verified by parsing the YAML.Note: this branch originally also carried the
update-docs/post-release-validation/pip index versionsfixes. Those landed via #50, so the branch was rebuilt on top ofmainand now contains only the change above.