> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pyx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Publishing packages

You can publish packages to your team's dedicated private registry on pyx.

There are two main ways to publish packages to pyx:

1. With an [authenticated](./authentication) client. This approach is
   recommended for teams that publish packages from development machines, or
   that are using CI/CD systems that do not (yet) support
   [Trusted Publishing](#trusted-publishing).
2. With [Trusted Publishing](#trusted-publishing). This approach is recommended
   for teams that publish packages from GitHub Actions and other supported CI/CD
   systems.

## Configure your publishing registry

Regardless of your publishing method, we recommend setting your registry's
`publish-url` in your `pyproject.toml` to avoid needing to specify URLs on the
command line.

For example, if your team is called `acme`, you can configure publishing to the
`acme/main` registry with:

```toml pyproject.toml highlight={2,4} icon="square-code" theme={null}
[[tool.uv.index]]
name = "main"
url = "https://api.pyx.dev/simple/acme/main"
publish-url = "https://api.pyx.dev/v1/upload/acme/main"
```

The `main` registry is created by default during onboarding, but you can always
[create additional registries](https://app.pyx.dev/registries) for your team.

## Publishing with an authenticated client

Once [authenticated](./authentication), you can publish packages to your team's
dedicated private registry.

For example, if you configured the `acme/main` registry as `main` (like above),
you can publish a package with:

```shell theme={null}
# 'main' is the name of the registry you configured above
uv publish /path/to/pyarrow-19.0.1.tar.gz --index=main
```

Alternatively, you can specify the `--publish-url` directly:

```shell theme={null}
uv publish /path/to/pyarrow-19.0.1.tar.gz --publish-url https://api.pyx.dev/v1/upload/acme/main
```

Publishing requires write access; access controls can be configured in the
[pyx dashboard](https://app.pyx.dev/team/users).

<Note>
  Use `uv publish --dry-run` to validate your distribution prior to writing to
  the registry.
</Note>

Upon completion, you can install the published package with, e.g.:

```shell theme={null}
uv pip install pyarrow --index https://api.pyx.dev/simple/acme/main
```

You can browse uploaded packages in the [pyx dashboard](https://app.pyx.dev), or
by querying the Simple API:

```shell theme={null}
export PYX_AUTH_TOKEN=$(uv auth token pyx.dev)
curl https://api.pyx.dev/simple/acme/main/pyarrow --header "Authorization: Bearer $PYX_TOKEN"
```

Or visit the Simple API URLs directly in your browser after logging in to the
[pyx dashboard](https://app.pyx.dev).

## Trusted Publishing

<Note>
  Trusted Publishing is currently in preview. All organizations have access to
  it, but you may encounter bugs or missing features. If you have feedback,
  please [get in touch](/contact).
</Note>

<Tip>
  If you're using Trusted Publishing to publish, you may also want to use
  Trusted Access to provide tokenless read-only access to your packages from
  CI/CD systems. See [Authentication - Trusted
  Access](/authentication#trusted-access) for details.
</Tip>

**Trusted Publishing** is an authentication method that uses
[OpenID Connect](https://openid.net/connect/) to allow CI/CD systems to upload
to package indices (in this case pyx) without needing to manage long-lived API
tokens.

In other words, with Trusted Publishing, there's no need to add or persist pyx
credentials in GitHub Actions or other supported CI/CD providers.

Trusted Publishing comes with usability and security benefits:

* **Usability**: with Trusted Publishing, there's no need to create and
  provision API tokens on your CI/CD system. The CI/CD system can authenticate
  directly to pyx.
* **Security**: Trusted Publishing uses short-lived credentials that are
  minimally scoped (down to the exact set of packages configured for
  publishing). These qualities reduce the blast radius of a compromised
  credential.

For additional information on Trusted Publishing, see the
[PyPI documentation](https://docs.pypi.org/trusted-publishers/) and the
OpenSSF's
[Trusted Publishers for All Package Repositories](https://repos.openssf.org/trusted-publishers-for-all-package-repositories.html).

### Supported Trusted Publishing providers

At the moment, pyx supports Trusted Publishing with the following CI/CD
providers:

* [GitHub Actions](https://github.com/features/actions)
* [BuildKite](https://buildkite.com/home/) (in closed beta)
* [GitLab CI/CD](https://docs.gitlab.com/ee/ci/) (in open beta)

<Note>
  At the moment, support for these providers is limited to their hosted
  offerings (e.g., `github.com`). Support for self-hosted instances is not yet
  available.
</Note>

### Enrolling a Trusted Publisher

You can enroll a Trusted Publisher against one or more of your team's packages
(or packages that you'd like created) in the in the
[pyx dashboard](https://app.pyx.dev) under
[Team > Trusted Publishers](https://app.pyx.dev/team/publishing).

<Note>
  At the moment, only team administrators can enroll Trusted Publishers.
</Note>

To add a Trusted Publisher, click the "Create Publisher" button and select the
projects you want to allow the publisher to upload to. Then, select the provider
you want to use and complete the steps for that provider.

<Tip>
  Unlike PyPI, pyx does not require a special "pending" publisher state: you can
  associate nonexistent packages with a Trusted Publisher, and pyx will create
  them automatically when the publisher first uploads a distribution to that
  package.
</Tip>

<Tabs>
  <Tab title="GitHub Actions" icon="github">
    A GitHub Actions Trusted Publisher has two mandatory components:

    * The **Repository** is the `owner/repo` slug on GitHub
      from which you'll be publishing.
    * The **Workflow** is the *filename* of the GitHub Actions workflow
      that will be publishing. This should be the base filename, e.g.,
      `publish.yml` (not `.github/workflows/publish.yml`).

    In addition, there are optional constraints you can add to your
    publisher:

    * The **Environment** is an *optional* name of the
      [GitHub Actions environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)
      that will be used for publishing. This environment must be configured
      in the GitHub repository settings and must be used in the workflow
      via `environment: <name>`.

          <Tip>
            Specifying an environment is optional, but recommended: environments
            enable you to enforce additional protections, like required reviewers
            and tag protection rules on your publishing workflow.
          </Tip>

    * The **Subject pattern** is an *optional*
      [fnmatch](https://docs.python.org/3/library/fnmatch.html)
      pattern that the `sub` claim in the GitHub OIDC token must match.
      See GitHub's [example subject claims](https://docs.github.com/en/actions/reference/security/oidc#example-subject-claims)
      for details.

          <Tip>
            Specifying a subject pattern is optional, but may be useful
            for enforcing publishing controls beyond those provided by
            an environment's protection rules.

            For example, you may wish to
            [customize the subject claim](https://docs.github.com/en/actions/reference/security/oidc#customizing-the-token-claims)
            to include the `job_workflow_ref`, and then use a subject pattern
            that matches a specific reusable workflow.
          </Tip>
  </Tab>

  <Tab title="BuildKite" icon="https://mintcdn.com/astral/_T7Zjz2ZqypN8m3Y/assets/buildkite-icon.svg?fit=max&auto=format&n=_T7Zjz2ZqypN8m3Y&q=85&s=ab1b3d9908eb94cbbc17c59cf62b0e03" width="480" height="320" data-path="assets/buildkite-icon.svg">
    <Note>
      BuildKite Trusted Publishing is currently in beta. If you'd like to
      participate in the beta, please [get in touch](/contact).
    </Note>

    A BuildKite Trusted Publisher has two mandatory components:

    * The **Organization** is the slug of the BuildKite organization
      from which you'll be publishing. This must match the `organization_slug`
      claim in the BuildKite OIDC token.

    * The **Pipeline** is the *slug* of the BuildKite pipeline
      that will be publishing.
      This must match the `pipeline_slug` claim in the BuildKite OIDC token.

    In addition, you can *optionally* specify the **Subject pattern**, which is an
    [fnmatch](https://docs.python.org/3/library/fnmatch.html) pattern that must
    match the `sub` claim in the BuildKite OIDC token.
  </Tab>

  <Tab title="GitLab CI/CD" icon="gitlab">
    A GitLab CI/CD Trusted Publisher has two mandatory components:

    * The **Repository** is the `namespace/project` slug on GitLab
      from which you'll be publishing. This slug can include groups and
      subgroups, e.g., `acme/anvils/my-project`.

    * The **Workflow** is the *filename* of the GitLab CI/CD
      pipeline configuration file that will be publishing. For most users
      this will be `.gitlab-ci.yml`, but may be a relative path to your
      pipeline configuration file if you're using
      [a custom location](https://docs.gitlab.com/ci/pipelines/settings/#specify-a-custom-cicd-configuration-file).

    In addition, there is an optional constraint you can add to your
    publisher:

    * The **Environment** is the *optional* name of the
      [GitLab environment](https://docs.gitlab.com/ee/ci/environments/)
      that will be used for publishing. This environment must be configured
      in the GitLab repository settings and must be used in the pipeline
      via `environment: <name>`.

    * The **Subject pattern** is an *optional*
      [fnmatch](https://docs.python.org/3/library/fnmatch.html)
      pattern that the `sub` claim in the GitLab OIDC token must match.
      See GitLab's [example token payload](https://docs.gitlab.com/ci/secrets/id_token_authentication/#token-payload)
      for details.
  </Tab>
</Tabs>

### Publishing with a Trusted Publisher

Once you've [enrolled](#enrolling-a-trusted-publisher) a Trusted Publisher
against one or more packages, you can publish to pyx with it.

Trusted Publishing to pyx is built directly into uv as of v0.9.27. To use it,
all you need to do is ensure that your CI/CD environment exposes the proper OIDC
token.

<Tabs>
  <Tab title="GitHub Actions" icon="github">
    <Note>
      For versions of uv prior to v0.9.27, you can use
      [astral-sh/pyx-auth-action](https://github.com/astral-sh/pyx-auth-action)
      to authenticate with pyx via Trusted Publishing. See the action's
      documentation for details.
    </Note>

    <Tip>
      For isolation reasons, we **strongly recommend** that you perform
      your distribution build in a *separate* job that your publishing job
      depends on. This ensures that your build environment does not have
      access to the short-lived credentials used for publishing.
    </Tip>

    ```yaml publish.yml icon="square-code" theme={null}
    jobs:
      build:
        runs-on: ubuntu-latest
        permissions:
          contents: read # for actions/checkout in private repos

        steps:
          - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
            with:
              persist-credentials: false

          - name: Set up uv
            uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
            with:
              version: ">=0.9.27"

          - name: Build distributions
            run: uv build

          - name: Upload distributions
            uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
            with:
              name: dists
              path: dist/
              if-no-files-found: error

      pyx-publish:
        runs-on: ubuntu-latest
        permissions:
          id-token: write # for Trusted Publishing to pyx

        # (Optional) Set the environment to match the environment defined in pyx.
        environment: pyx.dev

        steps:
          - name: Download distributions
            uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
            with:
              name: dists
              path: dist/

          # 'main' is the name of the registry you configured above
          - run: |
              uv publish --index=main --trusted-publishing=always
    ```
  </Tab>

  <Tab title="BuildKite" icon="https://mintcdn.com/astral/_T7Zjz2ZqypN8m3Y/assets/buildkite-icon.svg?fit=max&auto=format&n=_T7Zjz2ZqypN8m3Y&q=85&s=ab1b3d9908eb94cbbc17c59cf62b0e03" width="480" height="320" data-path="assets/buildkite-icon.svg">
    <Tip>
      Versions of uv prior to v0.9.27 do not directly support Trusted Publishing
      to pyx with BuildKite. See the [Manual integration](#manual-integration)
      section.
    </Tip>

    ```yaml pipeline.yml icon="square-code" theme={null}
    steps:
      - label: ":python: Publish to pyx"
        # 'main' is the name of the registry you configured above
        command: uv publish --index=main --trusted-publishing=always
        plugins:
          - docker:
              image: ghcr.io/astral-sh/uv:python3.14-bookworm-slim
              # Without these options, Buildkite's Docker plugin does not
              # propagate the agent (which supplies the OIDC token) into the container.
              propagate-environment: true
              mount-buildkite-agent: true
    ```
  </Tab>

  <Tab title="GitLab CI/CD" icon="gitlab">
    <Tip>
      Versions of uv prior to v0.9.27 do not directly support Trusted Publishing
      to pyx with GitLab CI/CD. See the [Manual integration](#manual-integration)
      section.
    </Tip>

    ```yaml .gitlab-ci.yml icon="square-code" theme={null}
    stages:
      - build
      - publish

    pyx-build:
      stage: build
      image: ghcr.io/astral-sh/uv:python3.14-bookworm-slim
      script:
        - uv build
      artifacts:
        paths:
          - dist/

    pyx-publish:
      stage: publish
      image: ghcr.io/astral-sh/uv:python3.14-bookworm-slim
      script:
        # 'main' is the name of the registry you configured above
        - uv publish --index=main --trusted-publishing=always
      environment: pyx.dev
    ```
  </Tab>

  <Tab title="Manual integration" icon="code">
    <Warning>
      You **do not need** to perform a manual integration if you're using
      uv v0.9.27 or newer, as Trusted Publishing to pyx is built directly into uv.
    </Warning>

    If you're using uv prior to v0.9.27 (or another client that does not
    directly support Trusted Publishing to pyx, like twine), you can still use Trusted
    Publishing by obtaining an OIDC token from your CI/CD provider and
    exchanging it for a pyx access token.

    <Steps>
      <Step title="Obtain the OIDC audience">
        Obtain the OIDC audience for pyx:

        ```shell theme={null}
        curl --silent "https://api.pyx.dev/v1/trusted-publishing/audience" \
          | jq -r .audience
        ```
      </Step>

      <Step title="Obtain an OIDC token from your CI/CD provider">
        Use the audience obtained in the previous step to get an OIDC token
        from your CI/CD provider.

        Every CI/CD provider has a different method for obtaining an OIDC token;
        refer to your provider's documentation for detail. For example:

        * [GitHub Actions: Methods for requesting the OIDC token](https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token)
        * [Buildkite: `buildkite-agent oidc`](https://buildkite.com/docs/agent/v3/cli/reference/oidc)
        * [GitLab CI/CD: Configure ID tokens in a CI/CD job](https://docs.gitlab.com/ci/secrets/id_token_authentication/#configure-id-tokens-in-a-cicd-job)

        You can use the [id](https://pypi.org/p/id) to automate this for many
        providers:

        ```shell theme={null}
        # ${audience} is the audience obtained in the previous step.
        uvx --from=id python -m id "${audience}"
        ```

        <Tip>
          If your CI/CD provider supports secret masking, we recommend
          enabling it for the OIDC token to avoid leaking it in logs.

          For example, for GitHub Actions:

          ```shell theme={null}
          echo "::add-mask::${oidc_token}"
          ```
        </Tip>
      </Step>

      <Step title="Exchange the OIDC token for a pyx access token">
        Once you have the OIDC token from your CI/CD provider, exchange it
        for a pyx access token:

        ```shell theme={null}
        oidc_token="..." # Step 2

        # acme/main is your workspace/registry on pyx
        resp=$(
          curl --silent -X POST \
            "https://api.pyx.dev/v1/trusted-publishing/acme/main/mint-token" \
            --json "{\"token\": \"${oidc_token}\"}"
        )
        api_token=$(jq -r '.token' <<< "${resp}")
        ```
      </Step>

      <Step title="Publish to pyx">
        Finally, use the obtained pyx access token to publish your
        distributions:

        ```shell theme={null}
        # 'main' is the name of the registry you configured above
        UV_PUBLISH_TOKEN="${api_token}" uv publish --index=main
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Private classifiers

pyx supports *private [classifiers](https://pypi.org/classifiers/)*, which you
can use to prevent packages from being accidentally published to PyPI (or
another index) instead of pyx.

Private classifiers come in two forms:

1. `Private :: pyx` allows publishing anywhere on pyx (but only to pyx).
2. `Private :: pyx :: <team>` restricts publishing to a specific
   [team](/concepts#teams). For example, `Private :: pyx :: acme` allows
   publishing only to the `acme` team's registries.

To use private classifiers, add them next to your other classifiers in your
`pyproject.toml`. For example:

```toml theme={null}
[project]
name = "top-secret"
version = "0.1.0"
classifiers = [
  "Programming Language :: Python :: 3",
  "Private :: pyx :: acme",
]
```

<Note>
  Like PyPI, pyx will reject packages that use the `Private :: Do Not Upload`
  classifier. Additionally, like PyPI, pyx will reject any `Private ::`
  classifiers not recognized by the rules above.
</Note>

## Twine support

pyx supports publishing from non-uv clients, like
[`twine`](https://twine.readthedocs.io/en/stable/). To publish with `twine`,
point the `twine` client to the pyx index, and authenticate via
`uv auth token pyx.dev`:

```shell theme={null}
export TWINE_REPOSITORY_URL=https://api.pyx.dev/v1/upload/acme/main
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=$(uv auth token pyx.dev)
twine upload /path/to/pyarrow-19.0.1.tar.gz
```

Or, on the command line:

```shell theme={null}
twine upload /path/to/pyarrow-19.0.1.tar.gz \
  --repository-url https://api.pyx.dev/v1/upload/acme/main \
  --username __token__ \
  --password $(uv auth token pyx.dev)
```
