Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions .github/workflows/publish-dev-to-testpypi.yml

This file was deleted.

44 changes: 18 additions & 26 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,43 +95,35 @@ jobs:
version: ${{ needs.prepare.outputs.version }}
artifact: dist-tagged

publish-to-testpypi:
needs: [prepare, assets]
uses: reqstool/.github/.github/workflows/python-publish-to-pypi.yml@main
permissions:
id-token: write
with:
target: testpypi
artifact: dist-tagged

# PyPI is the only step here that cannot be undone: a version can be yanked but
# never replaced. A release candidate stops at Test PyPI -- pip needs --pre to
# see a prerelease anyway.
# never replaced. Publishes release candidates too, with their pre-release
# identifier -- pip ignores them without --pre, so there is nothing unsafe
# about it landing on the real index; there is no separate staging index to
# route them to instead. Matches npm and Maven Central's existing behavior.
publish-to-pypi:
needs: [prepare, publish-to-testpypi]
if: ${{ needs.prepare.outputs.prerelease != 'true' }}
uses: reqstool/.github/.github/workflows/python-publish-to-pypi.yml@main
needs: [prepare, assets]
runs-on: ubuntu-latest
environment:
name: stable
url: https://pypi.org
permissions:
id-token: write
with:
target: pypi
artifact: dist-tagged
steps:
- uses: reqstool/.github/.github/actions/publish-to-pypi@b10b898cd5a1d552a578dbe4f170f84fb8f98b6c # main 2026-08-23
with:
artifact: dist-tagged

# Last, deliberately. Everything above can fail, and until this runs nothing
# resolving "the latest release" can see what was built -- the release is still
# a prerelease. Promotion itself is one API call against a release that already
# has its artifacts.
#
# The guard is `no job failed`, not the default `every job succeeded`: a release
# candidate deliberately skips the publish jobs that a real release runs, and a
# skipped dependency would otherwise cascade and skip this too -- leaving the
# candidate unpromoted, which is right, and every *real* release unpromoted the
# moment any optional job is skipped, which is not.
#
# `!inputs.dry-run` has to be spelled out for the same reason: on a dry run
# every job above is skipped, and "nothing failed" would otherwise be true.
# The guard is `no job failed`, not the default `every job succeeded`: on a dry
# run every job above is skipped, which would make plain success() false too --
# `!inputs.dry-run` is what actually gates this job then, and `no job failed`
# is what confirms nothing above it errored on a real run.
promote:
needs: [prepare, assets, publish-to-testpypi, publish-to-pypi]
needs: [prepare, assets, publish-to-pypi]
if: ${{ !inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
uses: reqstool/.github/.github/workflows/common-release-promote.yml@main
permissions:
Expand Down