diff --git a/docs/mkdocs-user-docs.yml b/docs/mkdocs-user-docs.yml index c69b439ebebe..aa7a07d81cb2 100644 --- a/docs/mkdocs-user-docs.yml +++ b/docs/mkdocs-user-docs.yml @@ -4,14 +4,16 @@ site_dir: user-site plugins: - macros: module_name: user/main - j2_block_start_string: '' - j2_variable_start_string: '' + j2_block_start_string: "" + j2_variable_start_string: "" markdown_extensions: - admonition - pymdownx.details - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true - tables theme: name: material @@ -46,20 +48,20 @@ edit_uri: blob/main/docs/user/ nav: - "index.md" - "Organization Accounts": - - "organization-accounts/index.md" - - "organization-accounts/org-acc-faq.md" - - "organization-accounts/roles-entities.md" - - "Actions": - - "organization-accounts/actions/billing-actions.md" - - "organization-accounts/actions/org-actions.md" - - "organization-accounts/actions/project-actions.md" - - "organization-accounts/actions/team-actions.md" - - "organization-accounts/pricing-and-payments.md" + - "organization-accounts/index.md" + - "organization-accounts/org-acc-faq.md" + - "organization-accounts/roles-entities.md" + - "Actions": + - "organization-accounts/actions/billing-actions.md" + - "organization-accounts/actions/org-actions.md" + - "organization-accounts/actions/project-actions.md" + - "organization-accounts/actions/team-actions.md" + - "organization-accounts/pricing-and-payments.md" - "Trusted Publishers": - - "trusted-publishers/index.md" - - "trusted-publishers/adding-a-publisher.md" - - "trusted-publishers/creating-a-project-through-oidc.md" - - "trusted-publishers/using-a-publisher.md" - - "trusted-publishers/security-model.md" - - "trusted-publishers/troubleshooting.md" - - "trusted-publishers/internals.md" + - "trusted-publishers/index.md" + - "trusted-publishers/adding-a-publisher.md" + - "trusted-publishers/creating-a-project-through-oidc.md" + - "trusted-publishers/using-a-publisher.md" + - "trusted-publishers/security-model.md" + - "trusted-publishers/troubleshooting.md" + - "trusted-publishers/internals.md" diff --git a/docs/user/assets/pending-publisher-form.png b/docs/user/assets/pending-publisher-form.png deleted file mode 100644 index 6688919d78fb..000000000000 Binary files a/docs/user/assets/pending-publisher-form.png and /dev/null differ diff --git a/docs/user/assets/project-publishing.png b/docs/user/assets/project-publishing.png deleted file mode 100644 index 5b2ea067fe2a..000000000000 Binary files a/docs/user/assets/project-publishing.png and /dev/null differ diff --git a/docs/user/trusted-publishers/adding-a-publisher.md b/docs/user/trusted-publishers/adding-a-publisher.md index 8b2915bc0d0e..ad8beea7582f 100644 --- a/docs/user/trusted-publishers/adding-a-publisher.md +++ b/docs/user/trusted-publishers/adding-a-publisher.md @@ -15,28 +15,34 @@ Then, click on "Publishing" in the project's sidebar: ![](/assets/project-publishing-link.png) -That link will take you to the publisher configuration page for the project: +That link will take you to the publisher configuration page for the project, +which will allow you to configure trusted publishers for the different +platforms supported by PyPI (such as GitHub Actions). -![](/assets/project-publishing.png) +To enable a publisher, you need to tell PyPI how to trust it. Each trusted +publisher has its own configuration requirements; click the tabs below to see +each. -To enable a publisher, you need to tell PyPI how to trust it. For -GitHub Actions (the only currently supported publisher), you do this by -providing the repository owner's name, the repository's name, and the -filename of the GitHub Actions workflow that's authorized to upload to -PyPI. +=== "GitHub Actions" -For example, if you have a project at `https://github.com/octo-org/sampleproject` -that uses a publishing workflow defined in `.github/workflows/release.yml` -and a custom environment named `release`, then you'd do the following: + For GitHub Actions, you **must** provide the repository owner's name, the + repository's name, and the filename of the GitHub Actions workflow that's + authorized to upload to PyPI. In addition, you may **optionally** + provide the name of a [GitHub Actions environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment). -![](/assets/project-publishing-form.png) + For example, if you have a project at `https://github.com/octo-org/sampleproject` + that uses a publishing workflow defined in `.github/workflows/release.yml` + and a custom environment named `release`, then you'd do the following: -!!! note + ![](/assets/project-publishing-form.png) + + !!! note + + Configuring an environment is optional, but **strongly** recommended: + with a GitHub environment, you can apply additional restrictions to + your trusted workflow, such as requiring manual approval on each run + by a trusted subset of repository maintainers. - Configuring an environment is optional, but **strongly** recommended: - with a GitHub environment, you can apply additional restrictions to - your trusted workflow, such as requiring manual approval on each run - by a trusted subset of repository maintainers. Once you click "Add", your publisher will be registered and will appear at the top of the page: diff --git a/docs/user/trusted-publishers/creating-a-project-through-oidc.md b/docs/user/trusted-publishers/creating-a-project-through-oidc.md index 152266169e91..27398d4ad7e1 100644 --- a/docs/user/trusted-publishers/creating-a-project-through-oidc.md +++ b/docs/user/trusted-publishers/creating-a-project-through-oidc.md @@ -14,25 +14,31 @@ that will *create* the project when used for the first time. "Pending" publishers are converted into "normal" publishers on first use, meaning that no further configuration is required. -The process for configuring a "pending" publisher are similar to those for +The steps for configuring a "pending" publisher are similar to those for a normal publisher, except that the page is under your account sidebar instead of any project's sidebar (since the project doesn't exist yet): ![](/assets/publishing-link.png) -Clicking on "publishing" will bring you to the following form: +Clicking on "publishing" will bring you to a page with different potential +trusted publishers. The forms on this page behave +the same as with publishers for existing projects, except that you also need to +provide the name of the PyPI project that will be created. -![](/assets/pending-publisher-form.png) +=== "GitHub Actions" -This form behaves the same as with publishers for existing projects, except that you -also need to provide the name of the PyPI project that will be created. + If you have a repository at + `https://github.com/octo-org/sampleproject` with a release workflow at + `release.yml` and an environment named `release` that you would like to publish + to PyPI as `sampleproject`, then you would do the following: -For example, if you have a repository at -`https://github.com/octo-org/sampleproject` with a release workflow at -`release.yml` and an environment named `release` that you would like to publish -to PyPI as `sampleproject`, then you would do the following: + ![](/assets/pending-publisher-form-filled.png) + + !!! note + + Like with "normal" trusted publishers, configuring a GitHub Actions + environment is **optional but strongly recommended**. -![](/assets/pending-publisher-form-filled.png) Clicking "Add" will register the "pending" publisher, and show it to you: diff --git a/docs/user/trusted-publishers/security-model.md b/docs/user/trusted-publishers/security-model.md index 313f56230bb8..a944bc89dbe7 100644 --- a/docs/user/trusted-publishers/security-model.md +++ b/docs/user/trusted-publishers/security-model.md @@ -4,31 +4,7 @@ title: Security Model and Considerations # Security model and considerations -## Security model - -GitHub Actions' own security model for OpenID Connect tokens is a little subtle: - -* Any workflow defined in a repository can request an OIDC token, - *with any audience*, **so long as it has the `id-token: write` permission**. - -* The claims defined in an OIDC token are *bound to the workflow*, meaning - that a workflow defined at `foo.yml` in `org/repo` **cannot impersonate** - a workflow defined at `bar.yml` in `org/repo`. However, if `foo.yml` is - *renamed* to `bar.yml`, then the *new* `bar.yml` will be indistinguishable - from the old `bar.yml` **except** for claims that reflect the repository's - state (e.g. `git` ref, branch, etc.). - -* *Generally speaking*, "third party" events **cannot** request an OIDC - token: even if they can trigger the workflow that requests the token, - the actual token retrieval step will fail. For example: PRs issued from forks - of a repository **cannot** access the OIDC tokens in the "upstream" - repository's workflows. - - * The exception to this is `pull_request_target` events, which are - **[fundamentally dangerous] by design** and should not be used without - careful consideration. - -## Considerations +## General considerations While more secure than passwords and long-lived API tokens, OIDC publishing is not a panacea. In particular: @@ -41,80 +17,119 @@ is not a panacea. In particular: one can mint API tokens against it for as long as it lives. * Configuring a trusted publisher means establishing trust in a particular piece - of external state; that state **must not** be controllable by untrusted - parties. In particular, for trusted publishing with GitHub Actions, you - **must**: + of external state (such as a GitHub Actions workflow); that state **must not** + be controllable by untrusted parties. + +In summary: treat your trusted publishers *as if* they were API tokens. If you +wouldn't let a user or piece of code access your API token, then they shouldn't +be able to invoke your trusted publisher. + +## Provider-specific considerations + +Each trusted publishing provider is its own OIDC identity provider, with its +own security model and considerations. + +=== "GitHub Actions" + + ## Security model + + GitHub Actions' own security model for OpenID Connect tokens is a little subtle: + + * Any workflow defined in a repository can request an OIDC token, + *with any audience*, **so long as it has the `id-token: write` permission**. + + * The claims defined in an OIDC token are *bound to the workflow*, meaning + that a workflow defined at `foo.yml` in `org/repo` **cannot impersonate** + a workflow defined at `bar.yml` in `org/repo`. However, if `foo.yml` is + *renamed* to `bar.yml`, then the *new* `bar.yml` will be indistinguishable + from the old `bar.yml` **except** for claims that reflect the repository's + state (e.g. `git` ref, branch, etc.). + + * *Generally speaking*, "third party" events **cannot** request an OIDC + token: even if they can trigger the workflow that requests the token, + the actual token retrieval step will fail. For example: PRs issued from forks + of a repository **cannot** access the OIDC tokens in the "upstream" + repository's workflows. + + * The exception to this is `pull_request_target` events, which are + **[fundamentally dangerous] by design** and should not be used without + careful consideration. + + ## Considerations + + * In particular, for trusted publishing with GitHub Actions, you + **must**: - * Trust the correct username and repository: if you trust a repository - other than one you control and trust, that repository can upload to your - PyPI project. + * Trust the correct username and repository: if you trust a repository + other than one you control and trust, that repository can upload to your + PyPI project. - * Trust the correct workflow: you shouldn't trust every workflow - to upload to PyPI; instead, you should isolate responsibility to the - smallest (and least-privileged) possible separate workflow. We recommend - naming this workflow `release.yml`. + * Trust the correct workflow: you shouldn't trust every workflow + to upload to PyPI; instead, you should isolate responsibility to the + smallest (and least-privileged) possible separate workflow. We recommend + naming this workflow `release.yml`. - * Take care when merging third-party changes to your code: if you trust - `release.yml`, then you must make sure that third-party changes to that - workflow (or code that runs within that workflow) are not malicious. + * Take care when merging third-party changes to your code: if you trust + `release.yml`, then you must make sure that third-party changes to that + workflow (or code that runs within that workflow) are not malicious. - * Take care when adding repository contributors, members, and administrators: - by default, anybody who can unconditionally commit to your repository can - also modify your publishing workflow to make it trigger on events you - may not intend (e.g., a manual `workflow_dispatch` trigger). + * Take care when adding repository contributors, members, and administrators: + by default, anybody who can unconditionally commit to your repository can + also modify your publishing workflow to make it trigger on events you + may not intend (e.g., a manual `workflow_dispatch` trigger). - This particular risk can be mitigated by using a dedicated environment - with manual approvers, as described below. + This particular risk can be mitigated by using a dedicated environment + with manual approvers, as described below. -* Trusted publishers are registered to projects, not to users. This means that - removing a user from a PyPI project does **not** remove any trusted publishers - that they might have registered, and that you should include a review - of any/all trusted publishers as part of "offboarding" a project maintainer. + * Trusted publishers are registered to projects, not to users. This means that + removing a user from a PyPI project does **not** remove any trusted publishers + that they might have registered, and that you should include a review + of any/all trusted publishers as part of "offboarding" a project maintainer. -PyPI has protections in place to make some attacks against OIDC more difficult -(like [account resurrection attacks]). However, like all forms of authentication, -the end user is **fundamentally responsible** for applying it correctly. + PyPI has protections in place to make some attacks against OIDC more difficult + (like [account resurrection attacks]). However, like all forms of authentication, + the end user is **fundamentally responsible** for applying it correctly. -In addition to the requirements above, you can do the following to -"ratchet down" the scope of your trusted publishing workflows: + In addition to the requirements above, you can do the following to + "ratchet down" the scope of your trusted publishing workflows: -* **Use per-job permissions**: The `permissions` key can be defined on the - workflow level or the job level; the job level is **always more secure** - because it limits the number of jobs that receive elevated `GITHUB_TOKEN` - credentials. + * **Use per-job permissions**: The `permissions` key can be defined on the + workflow level or the job level; the job level is **always more secure** + because it limits the number of jobs that receive elevated `GITHUB_TOKEN` + credentials. -* **[Use a dedicated environment]**: GitHub Actions supports "environments," - which can be used to isolate secrets to specific workflows. OIDC publishing - doesn't use any pre-configured secrets, but a dedicated `publish` or `deploy` - environment is a general best practice. + * **[Use a dedicated environment]**: GitHub Actions supports "environments," + which can be used to isolate secrets to specific workflows. OIDC publishing + doesn't use any pre-configured secrets, but a dedicated `publish` or `deploy` + environment is a general best practice. - Dedicated environments allow for additional protections like - [required reviewers], which can be used to require manual approval for a - workflow using the environment. + Dedicated environments allow for additional protections like + [required reviewers], which can be used to require manual approval for a + workflow using the environment. - For example, here is how `pypa/pip-audit`'s `release` environment - restricts reviews to members of the maintenance and admin teams: + For example, here is how `pypa/pip-audit`'s `release` environment + restricts reviews to members of the maintenance and admin teams: - ![](/assets/required-reviewers.png) + ![](/assets/required-reviewers.png) -* **[Use tag protection rules]**: if you use a tag-based publishing workflow - (e.g. triggering on tags pushed), then you can limit tag creation and - modification to maintainers and higher (or custom roles) for any tags - that match your release pattern. For example, `v*` will prevent - non-maintainers from creating or modifying tags that match version - strings like `v1.2.3`. + * **[Use tag protection rules]**: if you use a tag-based publishing workflow + (e.g. triggering on tags pushed), then you can limit tag creation and + modification to maintainers and higher (or custom roles) for any tags + that match your release pattern. For example, `v*` will prevent + non-maintainers from creating or modifying tags that match version + strings like `v1.2.3`. -* **Limit the scope of your publishing job**: your publishing job should - (ideally) have only two steps: + * **Limit the scope of your publishing job**: your publishing job should + (ideally) have only two steps: - 1. Retrieve the publishable distribution files from **a separate - build job**; + 1. Retrieve the publishable distribution files from **a separate + build job**; - 2. Publish the distributions using `pypa/gh-action-pypi-publish@release/v1`. + 2. Publish the distributions using `pypa/gh-action-pypi-publish@release/v1`. - By using a separate build job, you keep the number of steps that can - access the OIDC token to a bare minimum. This prevents both accidental - and malicious disclosure. + By using a separate build job, you keep the number of steps that can + access the OIDC token to a bare minimum. This prevents both accidental + and malicious disclosure. [fundamentally dangerous]: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ diff --git a/docs/user/trusted-publishers/using-a-publisher.md b/docs/user/trusted-publishers/using-a-publisher.md index 76dc79bf481d..763df16635c4 100644 --- a/docs/user/trusted-publishers/using-a-publisher.md +++ b/docs/user/trusted-publishers/using-a-publisher.md @@ -4,194 +4,208 @@ title: Publishing with a Trusted Publisher # Publishing with a Trusted Publisher -## The easy way - -Once you have a publisher configured, you can use the -PyPA's [`pypi-publish`](https://github.com/marketplace/actions/pypi-publish) -action to publish your packages. - -This looks *almost* exactly the same as normal, except that you don't -need any explicit usernames, passwords, or API tokens: GitHub's OIDC identity provider -will take care of everything for you: - -```yaml -jobs: - pypi-publish: - name: upload release to PyPI - runs-on: ubuntu-latest - # Specifying a GitHub environment is optional, but strongly encouraged - environment: release - permissions: - # IMPORTANT: this permission is mandatory for trusted publishing - id-token: write - steps: - # retrieve your distributions here - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 -``` - -If you're moving away from a password or API token-based authentication -flow, your diff might look like this: - -```diff - jobs: - pypi-publish: - name: upload release to PyPI - runs-on: ubuntu-latest -+ # Specifying a GitHub environment is optional, but strongly encouraged -+ environment: release -+ permissions: -+ # IMPORTANT: this permission is mandatory for trusted publishing -+ id-token: write - steps: - # retrieve your distributions here - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 -- with: -- username: __token__ -- password: ${{ secrets.PYPI_TOKEN }} -``` - -Note the `id-token: write` permission: you **must** provide this permission -at either the job level (**strongly recommended**) or workflow level -(**discouraged**). Without it, the publishing action -won't have sufficient permissions to identify itself to PyPI. - -!!! note - - Using the permission at the job level is **strongly** encouraged, as - it reduces unnecessary credential exposure. - -### Publishing to indices other than PyPI -The PyPA's [`pypi-publish`](https://github.com/marketplace/actions/pypi-publish) -action also supports trusted publishing with other (non-PyPI) indices, provided -they have trusted publishing enabled (and you've configured your trusted -publisher on them). For example, here's how you can use trusted publishing on -[TestPyPI](https://test.pypi.org): - -```yaml -- name: Publish package distributions to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ -``` - -## The manual way - -!!! warning - - **STOP! You probably don't need this section; it exists only to provide some - internal details about how GitHub Actions and PyPI coordinate using OIDC. - If you're an ordinary user, it is strongly recommended that you use the PyPA's - [`pypi-publish`](https://github.com/marketplace/actions/pypi-publish) - action instead.** - -The process for using an OIDC publisher is: - -1. Retrieve an *OIDC token* from the OIDC *identity provider*; -2. Submit that token to PyPI, which will return a short-lived API key; -3. Use that API key as you normally would (e.g. with `twine`) - -GitHub is currently the only OIDC identity provider supported, so we'll use it -for examples below. - -All code below assumes that it's being run in a GitHub Actions -workflow runner with `id-token: write` permissions. That permission is -**critical**; without it, GitHub Actions will refuse to give you an OIDC token. - -First, let's grab the OIDC token from GitHub Actions: - -```bash -resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ - "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") -``` - -**NOTE**: `audience=pypi` is only correct for PyPI. For TestPyPI, the correct -audience is `testpypi`. More generally, you can access any instance's expected -OIDC audience via the `{index}/_/oidc/audience` endpoint: - -```console -$ curl https://pypi.org/_/oidc/audience -{"audience":"pypi"} -``` +Once you have a trusted publisher configured on PyPI (whether "pending" or +"normal"), you can publish through it on the associated platform. The tabs +below describe the setup process for each supported trusted publisher. -The response to this will be a JSON blob, which contains the OIDC token. -We can pull it out using `jq`: +=== "GitHub Actions" -```bash -oidc_token=$(jq '.value' <<< "${resp}") -``` + ## The easy way -Finally, we can submit that token to PyPI and get a short-lived API token -back: - -```bash -resp=$(curl -X POST https://pypi.org/_/oidc/github/mint-token -d "{\"token\": \"${oidc_token}\"}") -api_token=$(jq '.token' <<< "${resp}") - -# tell GitHub Actions to mask the token in any console logs, -# to avoid leaking it -echo "::add-mask::${api_token}" -``` - -This API token can be fed into `twine` or any other uploading client: - -```bash -TWINE_USERNAME=__token__ TWINE_PASSWORD="${api_token}" twine upload dist/* -``` - -This can all be tied together into a single GitHub Actions workflow: - -```yaml -on: - release: - types: - - published - -name: release - -jobs: - pypi: - name: upload release to PyPI - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - uses: actions/checkout@v3 + You can use the PyPA's + [`pypi-publish`](https://github.com/marketplace/actions/pypi-publish) + action to publish your packages. + + This looks *almost* exactly the same as normal, except that you don't + need any explicit usernames, passwords, or API tokens: GitHub's OIDC identity provider + will take care of everything for you: + + ```yaml + jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + # Specifying a GitHub environment is optional, but strongly encouraged + environment: release + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + # retrieve your distributions here + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + ``` + + If you're moving away from a password or API token-based authentication + flow, your diff might look like this: + + ```diff + jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + + # Specifying a GitHub environment is optional, but strongly encouraged + + environment: release + + permissions: + + # IMPORTANT: this permission is mandatory for trusted publishing + + id-token: write + steps: + # retrieve your distributions here + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + - with: + - username: __token__ + - password: ${{ secrets.PYPI_TOKEN }} + ``` + + Note the `id-token: write` permission: you **must** provide this permission + at either the job level (**strongly recommended**) or workflow level + (**discouraged**). Without it, the publishing action + won't have sufficient permissions to identify itself to PyPI. + + !!! note + + Using the permission at the job level is **strongly** encouraged, as + it reduces unnecessary credential exposure. + + ### Publishing to indices other than PyPI + The PyPA's [`pypi-publish`](https://github.com/marketplace/actions/pypi-publish) + action also supports trusted publishing with other (non-PyPI) indices, provided + they have trusted publishing enabled (and you've configured your trusted + publisher on them). For example, here's how you can use trusted publishing on + [TestPyPI](https://test.pypi.org): + + ```yaml + - name: Publish package distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + ``` + + ## The manual way + + !!! warning + + **STOP! You probably don't need this section; it exists only to provide some + internal details about how GitHub Actions and PyPI coordinate using OIDC. + If you're an ordinary user, it is strongly recommended that you use the PyPA's + [`pypi-publish`](https://github.com/marketplace/actions/pypi-publish) + action instead.** + + !!! warning + + Many of the details described below are implementation-specific, + and are not subject to either a standardization process or + compatibility guarantees. They are not part of a public interface, + and may be changed at any time. For a stable public interface, + you **must** use the `pypi-publish` action. + + The process for using an OIDC publisher is: + + 1. Retrieve an *OIDC token* from the OIDC *identity provider*; + 2. Submit that token to PyPI, which will return a short-lived API key; + 3. Use that API key as you normally would (e.g. with `twine`) + + GitHub is currently the only OIDC identity provider supported, so we'll use it + for examples below. + + All code below assumes that it's being run in a GitHub Actions + workflow runner with `id-token: write` permissions. That permission is + **critical**; without it, GitHub Actions will refuse to give you an OIDC token. + + First, let's grab the OIDC token from GitHub Actions: + + ```bash + resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") + ``` + + **NOTE**: `audience=pypi` is only correct for PyPI. For TestPyPI, the correct + audience is `testpypi`. More generally, you can access any instance's expected + OIDC audience via the `{index}/_/oidc/audience` endpoint: + + ```console + $ curl https://pypi.org/_/oidc/audience + {"audience":"pypi"} + ``` + + The response to this will be a JSON blob, which contains the OIDC token. + We can pull it out using `jq`: + + ```bash + oidc_token=$(jq '.value' <<< "${resp}") + ``` + + Finally, we can submit that token to PyPI and get a short-lived API token + back: + + ```bash + resp=$(curl -X POST https://pypi.org/_/oidc/github/mint-token -d "{\"token\": \"${oidc_token}\"}") + api_token=$(jq '.token' <<< "${resp}") + + # tell GitHub Actions to mask the token in any console logs, + # to avoid leaking it + echo "::add-mask::${api_token}" + ``` + + This API token can be fed into `twine` or any other uploading client: + + ```bash + TWINE_USERNAME=__token__ TWINE_PASSWORD="${api_token}" twine upload dist/* + ``` + + This can all be tied together into a single GitHub Actions workflow: + + ```yaml + on: + release: + types: + - published - - uses: actions/setup-python@v4 - with: - python-version: "3.x" + name: release + + jobs: + pypi: + name: upload release to PyPI + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@v3 - - name: deps - run: python -m pip install -U build + - uses: actions/setup-python@v4 + with: + python-version: "3.x" - - name: build - run: python -m build + - name: deps + run: python -m pip install -U build - - name: mint API token - id: mint-token - run: | - # retrieve the ambient OIDC token - resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ - "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") - oidc_token=$(jq '.value' <<< "${resp}") + - name: build + run: python -m build - # exchange the OIDC token for an API token - resp=$(curl -X POST https://pypi.org/_/oidc/github/mint-token -d "{\"token\": \"${oidc_token}\"}") - api_token=$(jq '.token' <<< "${resp}") + - name: mint API token + id: mint-token + run: | + # retrieve the ambient OIDC token + resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") + oidc_token=$(jq '.value' <<< "${resp}") - # mask the newly minted API token, so that we don't accidentally leak it - echo "::add-mask::${api_token}" + # exchange the OIDC token for an API token + resp=$(curl -X POST https://pypi.org/_/oidc/github/mint-token -d "{\"token\": \"${oidc_token}\"}") + api_token=$(jq '.token' <<< "${resp}") + + # mask the newly minted API token, so that we don't accidentally leak it + echo "::add-mask::${api_token}" - # see the next step in the workflow for an example of using this step output - echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}" + # see the next step in the workflow for an example of using this step output + echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}" - - name: publish - # gh-action-pypi-publish uses TWINE_PASSWORD automatically - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ steps.mint-token.outputs.api-token }} -``` + - name: publish + # gh-action-pypi-publish uses TWINE_PASSWORD automatically + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ steps.mint-token.outputs.api-token }} + ```