Safeguard
Supply Chain•

Why should PyPI releases use separate build and publish jobs?

Build in one CI job, store the artifacts, and publish from a second job with only the permission it needs. Why that split limits a build compromise.

Safeguard Security Team
4 min read

Build your distributions in one job, save them as artifacts, and upload them from a second job that does nothing else. The publish job holds the identity PyPI trusts, so keeping build steps out of it limits what a compromised build tool or dependency can do.

What the packaging guide says

The Python Packaging User Guide's GitHub Actions walkthrough is explicit: keep the build job separate from the publishing jobs. It adds that building distributions in a publishing job is unsupported, and that publishing jobs should only download the already built artifacts and upload them.

The same guide uses trusted publishing, which exchanges short lived identity tokens instead of long lived API tokens. PyPI's docs say the minted token is valid for only 15 minutes, enough for CI to upload.

The idea in one picture

Build jobPublish job
Runs untrusted or complex codeYes, build tools and build dependenciesNo
Needs the OIDC permissionNoYes, id-token write
OutputArtifacts stored by the workflowUpload to PyPI
Can be reviewed asA normal CI jobA tiny, auditable job

Build jobs run your backend, your build requirements and often a plugin or two. That is a lot of third party code. If any of it is hostile, you do not want it executing in the same job that can request a publishing token.

A minimal shape

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - run: python3 -m pip install build --user
      - run: python3 -m build
      - uses: actions/upload-artifact@v5
        with:
          name: python-package-distributions
          path: dist/

  publish-to-pypi:
    needs: build
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    environment:
      name: pypi
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v6
        with:
          name: python-package-distributions
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1

This mirrors the structure in the guide. Check the guide for current action versions before you copy, as they change.

Details that matter

  • Environment approval. The guide says that for security reasons you must require manual approval on each run for the pypi environment. A human sees the tag and the artifacts before the token is minted.
  • Tag only publishing. The condition on tag refs stops branch pushes from reaching PyPI.
  • No stored credentials in checkout. The guide's example sets persist-credentials to false.
  • Separate TestPyPI job. The guide configures separate publishers and environments for TestPyPI and PyPI.
  • Attestations. With the official PyPA publish action, PyPI's docs say attestations are generated and uploaded by default, with no extra configuration.

For setup steps on the PyPI side, see our trusted publisher guide.

Try it yourself

Click through it at your own pace, or open it full screen.

Step through the gate and notice that checks run before anything is allowed through. A release job benefits from the same ordering.

Safeguard supports policies and gates for pull requests and pipelines. You can also try the free scan.

Quick check

Can the publish job also run the build?

The packaging guide says building in a publishing job is unsupported. Publishing jobs should download built artifacts and upload them.

How long does a trusted publishing token last?

PyPI's docs say 15 minutes from creation, enough for the upload.

Why require approval on the environment?

It puts a human check between a pushed tag and the upload, so a surprise run cannot publish silently.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.