Merge branch PR #306, GHSA-vxmw-7h4f-hqxh fix and PR #378 into unstable/v1

This commit is contained in:
Sviatoslav Sydorenko
2025-09-04 02:03:32 +02:00
7 changed files with 119 additions and 31 deletions
@@ -13,6 +13,8 @@ on: # yamllint disable-line rule:truthy
required: true
type: string
permissions: {}
jobs:
smoke-test:
uses: ./.github/workflows/reusable-smoke-test.yml
@@ -34,6 +36,8 @@ jobs:
jobs: ${{ toJSON(needs) }}
build-and-push:
permissions:
packages: write
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs:
@@ -41,6 +45,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build Docker image
run: |
DOCKER_TAG="${DOCKER_TAG/'/'/'-'}"
@@ -54,6 +54,7 @@ jobs:
uses: actions/checkout@v4
with:
path: test
persist-credentials: false
- name: Fail-fast in unsupported environments
continue-on-error: true
id: fail-fast
@@ -89,6 +90,7 @@ jobs:
uses: actions/checkout@v4
with:
path: test
persist-credentials: false
- name: Install the packaging-related tools
run: python3 -m pip install build twine
env:
+19
View File
@@ -0,0 +1,19 @@
---
name: GitHub Actions Security Analysis with zizmor 🌈
on: # yamllint disable-line rule:truthy
push:
pull_request:
jobs:
zizmor:
name: 🌈 zizmor
permissions:
security-events: write
# yamllint disable-line rule:line-length
uses: zizmorcore/workflow/.github/workflows/reusable-zizmor.yml@3bb5e95068d0f44b6d2f3f7e91379bed1d2f96a8
...
+8
View File
@@ -0,0 +1,8 @@
---
rules:
unpinned-uses:
config:
policies:
actions/*: ref-pin
github/*: ref-pin
re-actors/*: ref-pin
+1 -1
View File
@@ -109,7 +109,7 @@ repos:
WPS440,
WPS441,
WPS453,
- --max-module-members=8 # WPS202
- --per-file-ignores=attestations.py:WPS202 oidc-exchange.py:WPS202
additional_dependencies:
- flake8-2020 ~= 1.8.1
- flake8-pytest-style ~= 2.1.0
+23 -24
View File
@@ -100,7 +100,7 @@ runs:
exit 1
shell: bash -eEuo pipefail {0}
- name: Reset path if needed
run: |
run: | # zizmor: ignore[github-env] PATH is not user-controlled
# Reset path if needed
# https://github.com/pypa/gh-action-pypi-publish/issues/112
if [[ $PATH != *"/usr/bin"* ]]; then
@@ -111,25 +111,6 @@ runs:
echo "\$PATH reset. \$PATH=$PATH"
fi
shell: bash
- name: Set repo and ref from which to run Docker container action
id: set-repo-and-ref
run: |
# Set repo and ref from which to run Docker container action
# to handle cases in which `github.action_` context is not set
# https://github.com/actions/runner/issues/2473
REF=${{ env.ACTION_REF || env.PR_REF || github.ref_name }}
REPO=${{ env.ACTION_REPO || env.PR_REPO || github.repository }}
REPO_ID=${{ env.PR_REPO_ID || github.repository_id }}
echo "ref=$REF" >>"$GITHUB_OUTPUT"
echo "repo=$REPO" >>"$GITHUB_OUTPUT"
echo "repo-id=$REPO_ID" >>"$GITHUB_OUTPUT"
shell: bash
env:
ACTION_REF: ${{ github.action_ref }}
ACTION_REPO: ${{ github.action_repository }}
PR_REF: ${{ github.event.pull_request.head.ref }}
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
PR_REPO_ID: ${{ github.event.pull_request.base.repo.id }}
- name: Discover pre-installed Python
id: pre-installed-python
run: |
@@ -139,7 +120,8 @@ runs:
- name: Install Python 3
if: steps.pre-installed-python.outputs.python-path == ''
id: new-python
uses: actions/setup-python@v5
# yamllint disable-line rule:line-length
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: 3.x
- name: Create Docker container action
@@ -151,9 +133,26 @@ runs:
|| steps.pre-installed-python.outputs.python-path
}} '${{ github.action_path }}/create-docker-action.py'
env:
REF: ${{ steps.set-repo-and-ref.outputs.ref }}
REPO: ${{ steps.set-repo-and-ref.outputs.repo }}
REPO_ID: ${{ steps.set-repo-and-ref.outputs.repo-id }}
# Set repo and ref from which to run Docker container action
# to handle cases in which `github.action_` context is not set
# https://github.com/actions/runner/issues/2473
REF: >-
${{
github.action_ref
|| github.event.pull_request.head.ref
|| github.ref_name
}}
REPO: >-
${{
github.action_repository
|| github.event.pull_request.head.repo.full_name
|| github.repository
}}
REPO_ID: >-
${{
github.event.pull_request.base.repo.id
|| github.repository_id
}}
shell: bash
- name: Run Docker container
# The generated trampoline action must exist in the allowlisted
+60 -6
View File
@@ -2,9 +2,9 @@ import base64
import json
import os
import sys
import typing as t
from http import HTTPStatus
from pathlib import Path
from typing import NoReturn
from urllib.parse import urlparse
import id # pylint: disable=redefined-builtin
@@ -91,6 +91,30 @@ If a claim is not present in the claim set, then it is rendered as `MISSING`.
See https://docs.pypi.org/trusted-publishers/troubleshooting/ for more help.
"""
_REUSABLE_WORKFLOW_WARNING = """
The claims in this token suggest that the calling workflow is a reusable workflow.
In particular, this action was initiated by:
{job_workflow_ref}
Whereas its parent workflow is:
{workflow_ref}
Reusable workflows are **not currently supported** by PyPI's Trusted Publishing
functionality, and are subject to breakage. Users are **strongly encouraged**
to avoid using reusable workflows for Trusted Publishing until support
becomes official. Please, do not report bugs if this breaks.
For more information, see:
* https://docs.pypi.org/trusted-publishers/troubleshooting/#reusable-workflows-on-github
* https://github.com/pypa/gh-action-pypi-publish/issues/166 — subscribe to
this issue to watch the progress and learn when reusable workflows become
supported officially
"""
# Rendered if the package index's token response isn't valid JSON.
_SERVER_TOKEN_RESPONSE_MALFORMED_JSON = """
Token request failed: the index produced an unexpected
@@ -111,7 +135,7 @@ a few minutes and try again.
""" # noqa: S105; not a password
def die(msg: str) -> NoReturn:
def die(msg: str) -> t.NoReturn:
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
print(_ERROR_SUMMARY_MESSAGE.format(message=msg), file=io)
@@ -123,6 +147,14 @@ def die(msg: str) -> NoReturn:
sys.exit(1)
def warn(msg: str) -> None:
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
print(msg, file=io)
msg = msg.replace('\n', '%0A')
print(f'::warning::Potential workflow misconfiguration: {msg}', file=sys.stderr)
def debug(msg: str):
print(f'::debug::{msg.title()}', file=sys.stderr)
@@ -162,13 +194,15 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
)
def render_claims(token: str) -> str:
def extract_claims(token: str) -> dict[str, object]:
_, payload, _ = token.split('.', 2)
# urlsafe_b64decode needs padding; JWT payloads don't contain any.
payload += '=' * (4 - (len(payload) % 4))
claims = json.loads(base64.urlsafe_b64decode(payload))
return json.loads(base64.urlsafe_b64decode(payload))
def render_claims(claims: dict[str, object]) -> str:
def _get(name: str) -> str: # noqa: WPS430
return claims.get(name, 'MISSING')
@@ -184,6 +218,19 @@ def render_claims(token: str) -> str:
)
def warn_on_reusable_workflow(claims: dict[str, object]) -> None:
# A reusable workflow is identified by having different values
# for its workflow_ref (the initiating workflow) and job_workflow_ref
# (the reusable workflow).
workflow_ref = claims.get('workflow_ref')
job_workflow_ref = claims.get('job_workflow_ref')
if workflow_ref == job_workflow_ref:
return
warn(_REUSABLE_WORKFLOW_WARNING.format_map(locals()))
def event_is_third_party_pr() -> bool:
# Non-`pull_request` events cannot be from third-party PRs.
if os.getenv('GITHUB_EVENT_NAME') != 'pull_request':
@@ -225,12 +272,19 @@ try:
oidc_token = id.detect_credential(audience=oidc_audience)
except id.IdentityError as identity_error:
cause_msg_tmpl = (
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE if event_is_third_party_pr()
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE
if event_is_third_party_pr()
else _TOKEN_RETRIEVAL_FAILED_MESSAGE
)
for_cause_msg = cause_msg_tmpl.format(identity_error=identity_error)
die(for_cause_msg)
# Perform a non-fatal check to see if we're running on a reusable
# workflow, and emit a warning if so.
oidc_claims = extract_claims(oidc_token)
warn_on_reusable_workflow(oidc_claims)
# Now we can do the actual token exchange.
mint_token_resp = requests.post(
token_exchange_url,
@@ -257,7 +311,7 @@ if not mint_token_resp.ok:
for error in mint_token_payload['errors']
)
rendered_claims = render_claims(oidc_token)
rendered_claims = render_claims(oidc_claims)
die(
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE.format(