Skip to content

PyPI publish cannot be factored into a shared workflow or composite action #95

Description

@jimisola

Summary

Two independent GitHub Actions limitations mean PyPI publishing via trusted publishing
(OIDC) cannot be factored into a shared reusable component in this org — not a workflow,
and not a composite action either. Every PyPI-publishing repo has to inline both steps of
the publish job directly in its own release.yml. This issue documents why, for whoever
next tries to DRY this up.

Limitation 1: no reusable workflow

Upstream: pypi/warehouse#11096
"Trusted publishing: Support for GitHub reusable workflows", open, unresolved.

PyPI's trusted publisher matches OIDC claims against the workflow file the job actually
runs in. A workflow_call reusable workflow doesn't carry that identity the way PyPI
expects, so a job that dispatches pypa/gh-action-pypi-publish from inside one always fails
with invalid-publisher: valid token, but no corresponding publisher, regardless of what
the trusted publisher config on pypi.org names.

Confirmed against this org's own python-publish-to-pypi.yml on 2026-08-23: four real
release runs failed identically, and it explained why publish-dev-to-testpypi.yml had been
silently failing on every push to main since the 2026-08-16 migration.

The documented workaround, from a comment on the upstream
issue
pointing at
OpenAstronomy's
docs
:
build can stay in a reusable workflow, but the job calling pypa/gh-action-pypi-publish must
be defined directly in the caller's own workflow file.

Limitation 2: no composite action either

Not filed upstream as far as I've found — surfaced here first, from real release runs, the
same day.

The first fix (#92) moved the publish job out
of a reusable workflow and into a composite action, actions/publish-to-pypi, called
directly from each caller's release.yml. That satisfies limitation 1 — the job belongs to
the caller's workflow file for OIDC purposes — but pypa/gh-action-pypi-publish is a
Docker container action, and nesting one inside another uses: (a composite action, in
this case) breaks independently of the OIDC question.

Every real publish through actions/publish-to-pypi failed:

docker run ... ghcr.io/reqstool/.github:b10b898cd5a1d552a578dbe4f170f84fb8f98b6c
docker: invalid reference format

GitHub resolved the Docker image using the wrapping composite action's own repository and
pinned ref, not pypa/gh-action-pypi-publish's actual image. The action's own maintainers
say this usage is untested and
unsupported

which the OpenAstronomy example already implied, showing the publish step as a bare,
unwrapped step, not something we read closely enough the first time.

Fixed in #94 by deleting
actions/publish-to-pypi rather than trimming it: what would remain — one
actions/download-artifact call — isn't enough to justify a shared action on its own.

The workaround, in full

Every PyPI-publishing repo's release.yml now inlines both steps directly, with no shared
component for either:

  publish-to-pypi:
    needs: [prepare, assets]
    runs-on: ubuntu-latest
    environment:
      name: stable
      url: https://pypi.org
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v8.0.1
        with:
          name: dist-tagged
          path: dist
      - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
        with:
          attestations: true

This is the one publish mechanism in the org's release flow with no shared action or
workflow behind it — npm, Maven Central, the Gradle Plugin Portal and the VS Code
Marketplace all factor cleanly. Worth revisiting if pypi/warehouse#11096 is ever resolved,
or if pypa/gh-action-pypi-publish documents composite-action support as supported rather
than untested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions