Skip to content

Commit 40ff5ce

Browse files
authored
feat: Trusted Publishing to PyPI (#1706)
1 parent f96bfad commit 40ff5ce

File tree

2 files changed

+87
-20
lines changed

2 files changed

+87
-20
lines changed

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ The server type is selected using the `MAVEN_SERVER_ID` variable.
125125
| `ossrh` (deprecated) | `MAVEN_STAGING_PROFILE_ID` | Yes | Central Publisher (sonatype) staging profile ID, corresponding to namespace (e.g. `com.sonatype.software`). |
126126
| `ossrh` (deprecated) | `MAVEN_ENDPOINT` | No | URL of Nexus repository. Defaults to `https://central.sonatype.com`. |
127127

128-
**How to create a GPG key**
128+
### How to create a GPG key
129129

130130
Install [GnuPG](https://gnupg.org/).
131131

@@ -214,11 +214,31 @@ npx publib-pypi [DIR]
214214

215215
**Options (environment variables):**
216216

217-
| Option | Required | Description |
218-
| ---------------------- | -------- | -------------------------------------------------------------- |
219-
| `TWINE_USERNAME` | Required | PyPI username ([register](https://pypi.org/account/register/)) |
220-
| `TWINE_PASSWORD` | Required | PyPI password |
221-
| `TWINE_REPOSITORY_URL` | Optional | The registry URL (defaults to Twine default) |
217+
| Option | Required | Description |
218+
| --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
219+
| `TWINE_USERNAME` | Optional | PyPI username ([register](https://pypi.org/account/register/)). Not required when using Trusted Publishers. |
220+
| `TWINE_PASSWORD` | Optional | PyPI password or API token. Not required when using Trusted Publishers. |
221+
| `TWINE_REPOSITORY` | Optional | The repository to upload the package to. Defaults to `pypi`, set `testpypi` to publish to the testing index. |
222+
| `TWINE_REPOSITORY_URL` | Optional | A custom repository URL, overrides `TWINE_REPOSITORY`. |
223+
| `PYPI_TRUSTED_PUBLISHER` | Optional | Set to any value to use PyPI [Trusted Publisher](https://docs.pypi.org/trusted-publishers/) authentication (OIDC). Requires a supported ambient identity (i.e. CI/CD environment). |
224+
| `PYPI_DISABLE_ATTESTATIONS` | Optional | Set to any value to disable [PyPI attestations](https://docs.pypi.org/attestations/producing-attestations/) (enabled by default with Trusted Publishers). |
225+
226+
### Trusted Publishers and Attestations
227+
228+
PyPI [Trusted Publishers](https://docs.pypi.org/trusted-publishers/) allows publishing without API tokens by using OpenID Connect (OIDC) authentication between a trusted third-party service and PyPI.
229+
Typically these are CI/CD providers like GitHub Actions or Gitlab CI/CD.
230+
PyPI attestations provide cryptographic proof of package provenance and integrity and are **enabled by default when using Trusted Publishers**. Attestations are only available when using Trusted Publisher authentication.
231+
232+
**Trusted Publisher Setup:**
233+
234+
1. Configure your PyPI project to use a [Trusted Publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
235+
2. Set `PYPI_TRUSTED_PUBLISHER=1` in your workflow environment
236+
3. No `TWINE_USERNAME` or `TWINE_PASSWORD` needed
237+
238+
**Requirements:**
239+
240+
* **GitHub Actions**: Your workflow must have `id-token: write` permission.
241+
* **Gitlab CI/CD**: The keyword `id_tokens` is used to request an OIDC token from GitLab with name `PYPI_ID_TOKEN` and audience `pypi`.
222242

223243
## Golang
224244

bin/publib-pypi

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,74 @@ set -euo pipefail
99
#
1010
# DIR is where *.whl files are looked up (default is "dist/python")
1111
#
12-
# TWINE_USERNAME (required)
13-
# TWINE_PASSWORD (required)
12+
# TWINE_USERNAME (optional, ignored when using Trusted Publishers)
13+
# TWINE_PASSWORD (optional, ignored when using Trusted Publishers)
14+
# PYPI_TRUSTED_PUBLISHER (optional) - set to any value to use Trusted Publisher authentication
15+
# PYPI_DISABLE_ATTESTATIONS (optional) - set to any value to disable attestations
1416
#
1517
###
1618

1719
cd "${1:-"dist/python"}"
1820

19-
[ -z "${TWINE_USERNAME:-}" ] && {
20-
echo "Missing TWINE_USERNAME"
21-
exit 1
22-
}
23-
24-
[ -z "${TWINE_PASSWORD:-}" ] && {
25-
echo "Missing TWINE_PASSWORD"
26-
exit 1
27-
}
28-
2921
if [ -z "$(ls *.whl)" ]; then
3022
echo "cannot find any .whl files in $PWD"
3123
exit 1
3224
fi
3325

34-
python3 -m pip install --user --upgrade twine
35-
python3 -m twine upload --verbose --skip-existing *
26+
# Validate credentials before installing packages
27+
if [ -z "${PYPI_TRUSTED_PUBLISHER:-}" ]; then
28+
[ -z "${TWINE_USERNAME:-}" ] && {
29+
echo "Missing TWINE_USERNAME (required when not using Trusted Publishers)"
30+
exit 1
31+
}
32+
33+
[ -z "${TWINE_PASSWORD:-}" ] && {
34+
echo "Missing TWINE_PASSWORD (required when not using Trusted Publishers)"
35+
exit 1
36+
}
37+
fi
38+
39+
# Install required packages
40+
packages="twine"
41+
if [ -n "${PYPI_TRUSTED_PUBLISHER:-}" ]; then
42+
packages="$packages id"
43+
fi
44+
if [ -z "${PYPI_DISABLE_ATTESTATIONS:-}" ]; then
45+
packages="$packages pypi-attestations"
46+
fi
47+
python3 -m pip install --user --upgrade $packages
48+
49+
# Check for Trusted Publisher
50+
if [ -n "${PYPI_TRUSTED_PUBLISHER:-}" ]; then
51+
echo "Using PyPI Trusted Publisher authentication"
52+
53+
# Determine audience based on repository
54+
audience="pypi"
55+
mint_url="https://pypi.org/_/oidc/mint-token"
56+
if [ "${TWINE_REPOSITORY:-}" = "testpypi" ]; then
57+
audience="testpypi"
58+
mint_url="https://test.pypi.org/_/oidc/mint-token"
59+
fi
60+
61+
# Generate OIDC token and mint API token
62+
oidc_token=$(python3 -m id "$audience")
63+
resp=$(curl -s -X POST "$mint_url" -d "{\"token\": \"${oidc_token}\"}")
64+
api_token=$(jq -r '.token' <<< "${resp}")
65+
66+
export TWINE_USERNAME="__token__"
67+
export TWINE_PASSWORD="$api_token"
68+
fi
69+
70+
if [ -z "${PYPI_DISABLE_ATTESTATIONS:-}" ]; then
71+
echo "Signing packages with pypi-attestations"
72+
python3 -m pypi_attestations sign *
73+
fi
74+
75+
# Build upload command with optional attestations
76+
upload_opts="--verbose --skip-existing"
77+
if [ -z "${PYPI_DISABLE_ATTESTATIONS:-}" ]; then
78+
upload_opts="$upload_opts --attestations"
79+
fi
80+
81+
echo "Uploading packages to PyPI"
82+
python3 -m twine upload $upload_opts *

0 commit comments

Comments
 (0)