-
Notifications
You must be signed in to change notification settings - Fork 165
chore: Add OIDC-based PyPI token minting to CI workflow #7572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Introduces a step to mint a PyPI API token using GitHub Actions OIDC authentication, replacing the use of static secrets for publishing. Updates permissions and environment variables accordingly to enhance security and automation in the deployment process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR modernizes the CI/CD pipeline by introducing OIDC-based authentication for PyPI publishing, replacing static API credentials with dynamically minted tokens. This approach enhances security by eliminating long-lived secrets and leveraging GitHub Actions' native OIDC provider.
Key Changes:
- Added
id-token: writepermission to enable OIDC token retrieval - Implemented a token minting step that exchanges GitHub OIDC tokens for PyPI API tokens
- Updated Twine authentication to use dynamically generated tokens instead of stored secrets
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| oidc_token=$(jq -r '.value' <<< "${resp}") | ||
| # exchange the OIDC token for an API token | ||
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The curl command is missing the Content-Type header for the POST request. PyPI's OIDC mint-token endpoint expects 'Content-Type: application/json' when sending JSON data. Without this header, the request may fail or be rejected by the server.
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") | |
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"token\": \"${oidc_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 -r '.value' <<< "${resp}") | ||
| # exchange the OIDC token for an API token | ||
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") | ||
| api_token=$(jq -r '.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}" |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script doesn't set the shell to fail on error. Without 'set -euo pipefail' or similar error handling flags, the script will continue executing even if intermediate commands fail (like jq parsing or curl requests). This could lead to undefined behavior and make debugging difficult.
| resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | ||
| "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") | ||
| oidc_token=$(jq -r '.value' <<< "${resp}") | ||
| # exchange the OIDC token for an API token | ||
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The curl commands don't include the '--fail' or '--fail-with-body' flag. Without this, curl will return 0 (success) even for HTTP error responses (4xx, 5xx), making it impossible to detect API failures. This means invalid tokens or authentication errors won't be caught.
| resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | |
| "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") | |
| oidc_token=$(jq -r '.value' <<< "${resp}") | |
| # exchange the OIDC token for an API token | |
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") | |
| resp=$(curl --fail-with-body -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | |
| "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") | |
| oidc_token=$(jq -r '.value' <<< "${resp}") | |
| # exchange the OIDC token for an API token | |
| resp=$(curl --fail-with-body -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") |
| TWINE_USERNAME: __token__ | ||
| TWINE_PASSWORD: ${{ steps.mint-token.outputs.api-token }} | ||
| # We don't use `pants publish ::` because we manually rename the | ||
| # wheels after buildling them to add arch-specific tags. |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo in the comment. 'buildling' should be 'building'.
| # wheels after buildling them to add arch-specific tags. | |
| # wheels after building them to add arch-specific tags. |
| # exchange the OIDC token for an API token | ||
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") | ||
| api_token=$(jq -r '.token' <<< "${resp}") | ||
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The jq command extracts the token without validating that the field exists or contains a non-empty value. If the API response doesn't contain the expected field or returns an error structure, jq will output 'null' or an empty string, which will then be used as the API token. Consider adding validation to check that the extracted values are non-empty before proceeding.
| # exchange the OIDC token for an API token | |
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") | |
| api_token=$(jq -r '.token' <<< "${resp}") | |
| # validate that we successfully obtained an OIDC token | |
| if [ -z "${oidc_token}" ] || [ "${oidc_token}" = "null" ]; then | |
| echo "Error: failed to obtain OIDC token from GitHub OIDC endpoint." >&2 | |
| echo "Response was: ${resp}" >&2 | |
| exit 1 | |
| fi | |
| # exchange the OIDC token for an API token | |
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") | |
| api_token=$(jq -r '.token' <<< "${resp}") | |
| # validate that we successfully obtained a PyPI API token | |
| if [ -z "${api_token}" ] || [ "${api_token}" = "null" ]; then | |
| echo "Error: failed to obtain PyPI API token from mint-token endpoint." >&2 | |
| echo "Response was: ${resp}" >&2 | |
| exit 1 | |
| fi |
| resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | ||
| "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi") | ||
| oidc_token=$(jq -r '.value' <<< "${resp}") | ||
| # exchange the OIDC token for an API token | ||
| resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}") | ||
| api_token=$(jq -r '.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}" |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The token minting script lacks error handling. If either curl request fails or returns an error response, the script will continue and potentially set an empty or invalid token. This could lead to silent failures or exposure of error messages in subsequent steps. Consider adding error checking after each curl command to verify the response is valid before proceeding.
9552aac to
4af738e
Compare
Introduces a step to mint a PyPI API token using GitHub Actions OIDC authentication, replacing the use of static secrets for publishing. Updates permissions and environment variables accordingly to enhance security and automation in the deployment process.