2025, Dec 02 03:00

Why PyPI Trusted Publishing Fails with Reusable Workflows in GitHub Actions (HTTP 400) and How to Fix It

Learn why PyPI Trusted Publishing fails in GitHub Actions with reusable workflows, causing HTTP 400 on Test PyPI, and how to refactor with id-token: write.

Switching a GitHub Actions pipeline from a password-based upload to PyPI Trusted Publishing looks deceptively simple: drop the password input, grant id-token: write, and enjoy tokenless deploys. In practice, there is a sharp edge that can make the change appear to succeed while ending in an opaque 400 error from the publisher. The scenario below illustrates what happens when a development wheel is pushed to test.pypi.org/legacy using a Trusted Publisher via a reusable workflow, and why that fails.

Where the migration goes sideways

There is a job that builds and pushes a development wheel to Test PyPI. The previous setup passed because it used a password. After the migration, the password argument was removed and id-token: write permissions were added. The run executes, but the publish step returns HTTP 400 with no details.

name: dev-wheel-publish
on:
  push:
    branches:
      - main
jobs:
  release_dev_build:
    permissions:
      id-token: write
      contents: read
    uses: ./.github/workflows/reusable-publish.yml
    with:
      target-repo: https://test.pypi.org/legacy/

In this layout, the actual upload happens inside a reusable workflow, which is invoked via uses. The password has been removed and id-token: write is present, so everything looks aligned with Trusted Publishing. Yet the result is a 400.

Why it fails

The root cause is explicitly documented by PyPI. Trusted Publishers do not currently support being used through a reusable workflow. That practical limitation leads to the silent failure mode during publish.

Reusable workflows cannot currently be used as the workflow in a Trusted Publisher. This is a practical limitation, and is being tracked in warehouse#11096.

In other words, even if the Trusted Publisher is set up and the workflow has id-token: write, the OIDC flow will not work when routed through a reusable workflow. The result is the mysterious 400 during the publish step.

Refactoring the workflow to work with Trusted Publishing

The fix is to move the publish logic out of the reusable workflow and into a non-reusable workflow that runs in the repository configured as a Trusted Publisher. Keep id-token: write enabled on the job and avoid the password parameter entirely.

name: dev-wheel-publish
on:
  push:
    branches:
      - main
jobs:
  dev_whl_publish:
    permissions:
      id-token: write
      contents: read
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Build wheel
        run: |
          echo "build steps here"
      - name: Publish wheel to Test PyPI
        run: |
          echo "upload to https://test.pypi.org/legacy/"

This structure keeps the publisher job in a first-class workflow, not a reusable one. The key ingredients remain the same as in the attempted migration: the password input is gone and id-token: write is present.

One more practical point surfaced during review: without permissions: id-token: write on the job or workflow, Trusted Publishing will not work. In the example above, that permission is explicitly set. If a pipeline still fails, verify that id-token: write is declared at the correct scope for the job executing the publish step.

Why it’s worth knowing

Trusted Publishing eliminates credential sprawl and secret rotation in CI, which is precisely why teams migrate. Hitting a silent 400 on publish is a time sink if you do not know the limitation around reusable workflows. Recognizing this constraint lets you plan the refactor upfront instead of chasing a non-diagnostic error.

Conclusion

If a Test PyPI or PyPI upload works with a password but fails with a Trusted Publisher and returns a 400, check whether the publisher runs through a reusable workflow. PyPI currently does not support that pattern. Move the publishing job into a non-reusable workflow in the trusted repository, ensure permissions: id-token: write is present where the publish runs, and keep the password removed. That small structural change aligns the pipeline with PyPI’s Trusted Publishing requirements and restores a clean, secretless release path.