diff --git a/.github/actions/download-dists/action.yml b/.github/actions/download-dists/action.yml new file mode 100644 index 0000000..f61df4d --- /dev/null +++ b/.github/actions/download-dists/action.yml @@ -0,0 +1,45 @@ +name: Download distributions +description: > + Download a built Python distribution artifact into dist/, ready for a publish + step in the calling job. + +# Deliberately does NOT publish, despite being the shared half of the PyPI +# publish job. pypa/gh-action-pypi-publish is a Docker container action, and +# GitHub resolves a nested Docker action's image against the *wrapping* action's +# repository rather than its own: an earlier version of this action wrapped it, +# and every publish failed with `docker: invalid reference format`, trying to +# pull ghcr.io/reqstool/.github:. The action's own +# maintainer describes the same breakage in pypi/warehouse#11096. The publish +# step therefore stays inline in each caller's release.yml. +# +# A composite action remains the right shape for the rest. Unlike a +# workflow_call reusable workflow, it does not create a new workflow context, so +# the job keeps the caller's own OIDC identity -- which is what PyPI's trusted +# publishing matches on, and why the publish job cannot live in a reusable +# workflow. See RELEASING.md. + +inputs: + dry-run: + description: "Validate the artifacts with twine check instead of leaving them for a publish step." + required: false + default: "false" + artifact: + description: "Name of the build artifact holding the distributions." + required: false + default: "dist" + +runs: + using: composite + steps: + - uses: actions/download-artifact@v8.0.1 + with: + name: ${{ inputs.artifact }} + path: dist + + - name: Dry-run — validate artifacts + if: ${{ inputs.dry-run == 'true' }} + shell: bash + run: | + # renovate: datasource=pypi depName=twine + pip install --quiet twine==7.0.0 + twine check --strict dist/* diff --git a/.github/actions/publish-to-pypi/action.yml b/.github/actions/publish-to-pypi/action.yml deleted file mode 100644 index 4adb1e5..0000000 --- a/.github/actions/publish-to-pypi/action.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Publish to PyPI -description: Upload a built distribution to PyPI via trusted publishing (OIDC). - -# PyPI does not support trusted publishing through a workflow_call reusable -# workflow -- https://github.com/pypi/warehouse/issues/11096, confirmed against -# this org's own release runs on 2026-08-23. A composite action doesn't have -# that problem: the OIDC claim is about which *workflow file* the job runs in, -# and a job that calls a composite action for its steps still belongs to the -# caller's own workflow file. So the upload step lives here, but the job that -# calls this action -- with its `environment:` and `permissions: id-token: -# write` -- must be defined directly in the caller's own workflow, never behind -# `uses: reqstool/.github/.github/workflows/...@main`. -# -# This replaces python-publish-to-pypi.yml for that reason. That workflow is -# kept only for tests/python/publish-to-pypi.yml's actionlint coverage; nothing -# should call it for a real publish. -# -# No Test PyPI target: PyPI accepts pre-release version identifiers (0.3.0rc1) -# on the real index directly, and pip ignores them without --pre, so a -# candidate never needs a separate index to land somewhere safe. - -inputs: - dry-run: - description: "Run twine check instead of uploading." - required: false - default: "false" - artifact: - description: "Name of the build artifact holding the distributions." - required: false - default: "dist" - -runs: - using: composite - steps: - - uses: actions/download-artifact@v8.0.1 - with: - name: ${{ inputs.artifact }} - path: dist - - - name: Dry-run — validate artifacts - if: ${{ inputs.dry-run == 'true' }} - shell: bash - run: | - # renovate: datasource=pypi depName=twine - pip install --quiet twine==7.0.0 - twine check --strict dist/* - - - name: Publish to PyPI - if: ${{ inputs.dry-run != 'true' }} - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - with: - attestations: true diff --git a/RELEASING.md b/RELEASING.md index 40ead81..42e4329 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -123,15 +123,27 @@ cleaner option, as below. > pypi.org (project → Publishing) must read `stable`. The same applies to any other registry > that binds an OIDC identity to an environment name. -> **PyPI does not accept a reusable workflow as the trusted publisher at all**, not even with -> the right names — -> [pypi/warehouse#11096](https://github.com/pypi/warehouse/issues/11096), unresolved as of -> this writing, confirmed against this org's own release runs on 2026-08-23. The job that -> calls `pypa/gh-action-pypi-publish` must be defined directly in the caller's own workflow -> file; `actions/publish-to-pypi` is a composite action, not a `workflow_call` workflow, for -> exactly this reason — a job that uses it for steps still belongs to the caller's own -> workflow for OIDC purposes. Every PyPI-publishing repo's `release.yml` must call it this -> way, never through another reusable workflow. +> **The PyPI publish job cannot live in a reusable workflow in this repo.** PyPI matches the +> OIDC `job_workflow_ref` claim — the *bottom-most* workflow that ran the job — against the +> Trusted Publisher's owner, repo and filename. A `workflow_call` into +> `reqstool/.github` puts *this* repository in that claim, which can never match a Trusted +> Publisher configured for the calling project, so it fails with `invalid-publisher` no matter +> what the config names. (A reusable workflow in the *same* repo as its caller does work, by +> accident of the same matching rule — that is not what this org does.) +> [pypi/warehouse#11096](https://github.com/pypi/warehouse/issues/11096) tracks proper support; +> unresolved as of this writing. +> +> **A composite action is fine, and is what this repo uses.** Unlike a reusable workflow it +> creates no new workflow context, so the job keeps the caller's own identity — hence +> `actions/download-dists`. +> +> **But `pypa/gh-action-pypi-publish` may not be nested inside one.** It is a Docker container +> action, and GitHub resolves a nested Docker action's image against the wrapping action's +> repository rather than its own — an earlier version of `download-dists` wrapped it and every +> publish failed with `docker: invalid reference format`. Its own maintainer +> [describes the same breakage](https://github.com/pypi/warehouse/issues/11096#issuecomment-2871981512) +> ("GH doesn't work well with nested composite actions ... making it defunct"). It stays a bare +> step, inline, in every PyPI-publishing repo's `release.yml`. ## Cutting a release candidate @@ -200,7 +212,8 @@ prepare (dry-run stops here) → [approval] tag common-release-tag.yml → build @ tag the repo's own build.yml, ref = the tag → assets common-release-assets.yml - → publish actions/publish-to-pypi (job in the caller's own workflow) + → publish actions/download-dists + an inline publish step, + in the caller's own workflow (PyPI) / java-publish-to-maven.yml / … → promote common-release-promote.yml ```