From 394e2f3fa882f50f288a6550ba74940a23ef8af2 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 23 Aug 2026 16:10:24 +0200 Subject: [PATCH 1/2] fix(ci): stop nesting pypa/gh-action-pypi-publish in a composite action Confirmed against four real release runs today: pypa/gh-action-pypi-publish is a Docker container action, and GitHub resolves its image using the wrapping action's own repository and ref rather than the Docker action's when nested inside another `uses:` -- every publish failed with `docker: invalid reference format`, trying to pull ghcr.io/reqstool/.github:. The action's own maintainers say this usage is untested and unsupported. This is exactly what the workaround comment linked from #92's PR description already said -- upload as a bare step in the caller, not routed through anything else -- and actions/publish-to-pypi violated it by wrapping the publish step instead of just inlining it. Deleting the composite action rather than trimming it to only download-artifact: what remains is one actions/download-artifact call, not enough indirection to be worth a shared action, and every layer added here tonight has broken in a new way. Companion PRs inline both steps directly into each PyPI-publishing repo's release.yml. Signed-off-by: Jimisola Laursen --- .github/actions/publish-to-pypi/action.yml | 52 ---------------------- RELEASING.md | 17 ++++--- 2 files changed, 12 insertions(+), 57 deletions(-) delete mode 100644 .github/actions/publish-to-pypi/action.yml 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..9dfb65d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -128,10 +128,17 @@ cleaner option, as below. > [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. +> file, never behind `uses: reqstool/.github/.github/workflows/...@main`. +> +> **It cannot be wrapped in a composite action either** — also confirmed against this org's +> own runs, the same day. `pypa/gh-action-pypi-publish` is a Docker container action, and +> GitHub resolves its image using the *wrapping* action's own repository rather than +> `pypa/gh-action-pypi-publish`'s when it is nested inside another `uses:`, which fails with +> `docker: invalid reference format`. [The action's own maintainers say this usage is +> untested and unsupported](https://github.com/pypa/gh-action-pypi-publish/blob/unstable/v1/README.md). +> It has to appear as a bare step, inline, in every PyPI-publishing repo's `release.yml` — +> which is also why there is no shared composite action for the PyPI publish step at all, +> unlike every other registry this org publishes to. ## Cutting a release candidate @@ -200,7 +207,7 @@ 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 inline steps in the caller's own workflow (PyPI) / java-publish-to-maven.yml / … → promote common-release-promote.yml ``` From 5a23b2d44f91fb5c7db868152d910dc3f4616acc Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 23 Aug 2026 16:25:29 +0200 Subject: [PATCH 2/2] refactor(ci): keep the composite action, drop only the publish step Reworks the previous commit, which deleted the action outright on the mistaken premise that composite actions were the problem. They are not: a composite action creates no new workflow context, so the job keeps the caller's OIDC identity -- which is exactly why it works where a reusable workflow does not. Someone makes the same point for Ruby in pypi/warehouse#11096. Only pypa/gh-action-pypi-publish has to come out. 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 -- the action's own maintainer describes the same breakage in that thread. Renamed publish-to-pypi -> download-dists, since what remains downloads the distributions and deliberately does not publish them. Keeping the old name would have been the misleading part. RELEASING.md's PyPI notes corrected while here. Two claims in the previous commit were wrong: PyPI matches `job_workflow_ref`, the bottom-most workflow, not the top-level caller; and reusable workflows are not rejected categorically -- one in the *same* repo as its caller works by accident of that same rule. What fails is a cross-repo one like this repo's, which puts reqstool/.github in the claim where the calling project has to be. Signed-off-by: Jimisola Laursen --- .github/actions/download-dists/action.yml | 45 +++++++++++++++++++++++ RELEASING.md | 38 +++++++++++-------- 2 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 .github/actions/download-dists/action.yml 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/RELEASING.md b/RELEASING.md index 9dfb65d..42e4329 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -123,22 +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, never behind `uses: reqstool/.github/.github/workflows/...@main`. +> **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. > -> **It cannot be wrapped in a composite action either** — also confirmed against this org's -> own runs, the same day. `pypa/gh-action-pypi-publish` is a Docker container action, and -> GitHub resolves its image using the *wrapping* action's own repository rather than -> `pypa/gh-action-pypi-publish`'s when it is nested inside another `uses:`, which fails with -> `docker: invalid reference format`. [The action's own maintainers say this usage is -> untested and unsupported](https://github.com/pypa/gh-action-pypi-publish/blob/unstable/v1/README.md). -> It has to appear as a bare step, inline, in every PyPI-publishing repo's `release.yml` — -> which is also why there is no shared composite action for the PyPI publish step at all, -> unlike every other registry this org publishes to. +> **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 @@ -207,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 inline steps in the caller's own workflow (PyPI) + → 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 ```