1010import id # pylint: disable=redefined-builtin
1111import requests
1212
13- _GITHUB_STEP_SUMMARY = Path (os .getenv (" GITHUB_STEP_SUMMARY" ))
13+ _GITHUB_STEP_SUMMARY = Path (os .getenv (' GITHUB_STEP_SUMMARY' ))
1414
1515# The top-level error message that gets rendered.
1616# This message wraps one of the other templates/messages defined below.
4545```
4646
4747Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings.
48- """
48+ """ # noqa: S105; not a password
4949
5050# Specialization of the token retrieval failure case, when we know that
5151# the failure cause is use within a third-party PR.
5959To fix this, change your publishing workflow to use an event that
6060forks of your repository cannot trigger (such as tag or release
6161creation, or a manually triggered workflow dispatch).
62- """
62+ """ # noqa: S105; not a password
6363
6464# Rendered if the package index refuses the given OIDC token.
6565_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE = """
7171also indicate an internal error on GitHub or PyPI's part.
7272
7373{rendered_claims}
74- """
74+ """ # noqa: S105; not a password
7575
7676_RENDERED_CLAIMS = """
7777The claims rendered below are **for debugging purposes only**. You should **not**
9797
9898This strongly suggests a server configuration or downtime issue; wait
9999a few minutes and try again.
100- """
100+ """ # noqa: S105; not a password
101101
102102# Rendered if the package index's token response isn't a valid API token payload.
103103_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE = """
104104Token response error: the index gave us an invalid response.
105105
106106This strongly suggests a server configuration or downtime issue; wait
107107a few minutes and try again.
108- """
108+ """ # noqa: S105; not a password
109109
110110
111111def die (msg : str ) -> NoReturn :
112- with _GITHUB_STEP_SUMMARY .open ("a" , encoding = " utf-8" ) as io :
112+ with _GITHUB_STEP_SUMMARY .open ('a' , encoding = ' utf-8' ) as io :
113113 print (_ERROR_SUMMARY_MESSAGE .format (message = msg ), file = io )
114114
115115 # HACK: GitHub Actions' annotations don't work across multiple lines naively;
116116 # translating `\n` into `%0A` (i.e., HTML percent-encoding) is known to work.
117117 # See: https://github.com/actions/toolkit/issues/193
118- msg = msg .replace (" \n " , " %0A" )
119- print (f" ::error::Trusted publishing exchange failure: { msg } " , file = sys .stderr )
118+ msg = msg .replace (' \n ' , ' %0A' )
119+ print (f' ::error::Trusted publishing exchange failure: { msg } ' , file = sys .stderr )
120120 sys .exit (1 )
121121
122122
123123def debug (msg : str ):
124- print (f" ::debug::{ msg .title ()} " , file = sys .stderr )
124+ print (f' ::debug::{ msg .title ()} ' , file = sys .stderr )
125125
126126
127127def get_normalized_input (name : str ) -> str | None :
128- name = f" INPUT_{ name .upper ()} "
128+ name = f' INPUT_{ name .upper ()} '
129129 if val := os .getenv (name ):
130130 return val
131- return os .getenv (name .replace ("-" , "_" ))
131+ return os .getenv (name .replace ('-' , '_' ))
132132
133133
134134def assert_successful_audience_call (resp : requests .Response , domain : str ):
@@ -140,81 +140,81 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
140140 # This index supports OIDC, but forbids the client from using
141141 # it (either because it's disabled, ratelimited, etc.)
142142 die (
143- f" audience retrieval failed: repository at { domain } has trusted publishing disabled" ,
143+ f' audience retrieval failed: repository at { domain } has trusted publishing disabled' ,
144144 )
145145 case HTTPStatus .NOT_FOUND :
146146 # This index does not support OIDC.
147147 die (
148- " audience retrieval failed: repository at "
149- f" { domain } does not indicate trusted publishing support" ,
148+ ' audience retrieval failed: repository at '
149+ f' { domain } does not indicate trusted publishing support' ,
150150 )
151151 case other :
152152 status = HTTPStatus (other )
153153 # Unknown: the index may or may not support OIDC, but didn't respond with
154154 # something we expect. This can happen if the index is broken, in maintenance mode,
155155 # misconfigured, etc.
156156 die (
157- " audience retrieval failed: repository at "
158- f" { domain } responded with unexpected { other } : { status .phrase } " ,
157+ ' audience retrieval failed: repository at '
158+ f' { domain } responded with unexpected { other } : { status .phrase } ' ,
159159 )
160160
161161
162162def render_claims (token : str ) -> str :
163- _ , payload , _ = token .split ("." , 2 )
163+ _ , payload , _ = token .split ('.' , 2 )
164164
165165 # urlsafe_b64decode needs padding; JWT payloads don't contain any.
166- payload += "=" * (4 - (len (payload ) % 4 ))
166+ payload += '=' * (4 - (len (payload ) % 4 ))
167167 claims = json .loads (base64 .urlsafe_b64decode (payload ))
168168
169169 def _get (name : str ) -> str : # noqa: WPS430
170- return claims .get (name , " MISSING" )
170+ return claims .get (name , ' MISSING' )
171171
172172 return _RENDERED_CLAIMS .format (
173- sub = _get (" sub" ),
174- repository = _get (" repository" ),
175- repository_owner = _get (" repository_owner" ),
176- repository_owner_id = _get (" repository_owner_id" ),
177- job_workflow_ref = _get (" job_workflow_ref" ),
178- ref = _get (" ref" ),
173+ sub = _get (' sub' ),
174+ repository = _get (' repository' ),
175+ repository_owner = _get (' repository_owner' ),
176+ repository_owner_id = _get (' repository_owner_id' ),
177+ job_workflow_ref = _get (' job_workflow_ref' ),
178+ ref = _get (' ref' ),
179179 )
180180
181181
182182def event_is_third_party_pr () -> bool :
183183 # Non-`pull_request` events cannot be from third-party PRs.
184- if os .getenv (" GITHUB_EVENT_NAME" ) != " pull_request" :
184+ if os .getenv (' GITHUB_EVENT_NAME' ) != ' pull_request' :
185185 return False
186186
187- event_path = os .getenv (" GITHUB_EVENT_PATH" )
187+ event_path = os .getenv (' GITHUB_EVENT_PATH' )
188188 if not event_path :
189189 # No GITHUB_EVENT_PATH indicates a weird GitHub or runner bug.
190- debug (" unexpected: no GITHUB_EVENT_PATH to check" )
190+ debug (' unexpected: no GITHUB_EVENT_PATH to check' )
191191 return False
192192
193193 try :
194194 event = json .loads (Path (event_path ).read_bytes ())
195195 except json .JSONDecodeError :
196- debug (" unexpected: GITHUB_EVENT_PATH does not contain valid JSON" )
196+ debug (' unexpected: GITHUB_EVENT_PATH does not contain valid JSON' )
197197 return False
198198
199199 try :
200- return event [" pull_request" ][ " head" ][ " repo" ][ " fork" ]
200+ return event [' pull_request' ][ ' head' ][ ' repo' ][ ' fork' ]
201201 except KeyError :
202202 return False
203203
204204
205- repository_url = get_normalized_input (" repository-url" )
205+ repository_url = get_normalized_input (' repository-url' )
206206repository_domain = urlparse (repository_url ).netloc
207- token_exchange_url = f" https://{ repository_domain } /_/oidc/mint-token"
207+ token_exchange_url = f' https://{ repository_domain } /_/oidc/mint-token'
208208
209209# Indices are expected to support `https://{domain}/_/oidc/audience`,
210210# which tells OIDC exchange clients which audience to use.
211- audience_url = f" https://{ repository_domain } /_/oidc/audience"
212- audience_resp = requests .get (audience_url )
211+ audience_url = f' https://{ repository_domain } /_/oidc/audience'
212+ audience_resp = requests .get (audience_url , timeout = 5 ) # S113 wants a timeout
213213assert_successful_audience_call (audience_resp , repository_domain )
214214
215- oidc_audience = audience_resp .json ()[" audience" ]
215+ oidc_audience = audience_resp .json ()[' audience' ]
216216
217- debug (f" selected trusted publishing exchange endpoint: { token_exchange_url } " )
217+ debug (f' selected trusted publishing exchange endpoint: { token_exchange_url } ' )
218218
219219try :
220220 oidc_token = id .detect_credential (audience = oidc_audience )
@@ -229,7 +229,8 @@ def event_is_third_party_pr() -> bool:
229229# Now we can do the actual token exchange.
230230mint_token_resp = requests .post (
231231 token_exchange_url ,
232- json = {"token" : oidc_token },
232+ json = {'token' : oidc_token },
233+ timeout = 5 , # S113 wants a timeout
233234)
234235
235236try :
@@ -246,9 +247,9 @@ def event_is_third_party_pr() -> bool:
246247# On failure, the JSON response includes the list of errors that
247248# occurred during minting.
248249if not mint_token_resp .ok :
249- reasons = " \n " .join (
250- f" * `{ error [' code' ]} `: { error [' description' ] } "
251- for error in mint_token_payload [" errors" ]
250+ reasons = ' \n ' .join (
251+ f' * `{ error [" code" ]} `: { error [" description" ] } '
252+ for error in mint_token_payload [' errors' ]
252253 )
253254
254255 rendered_claims = render_claims (oidc_token )
@@ -260,12 +261,12 @@ def event_is_third_party_pr() -> bool:
260261 ),
261262 )
262263
263- pypi_token = mint_token_payload .get (" token" )
264+ pypi_token = mint_token_payload .get (' token' )
264265if pypi_token is None :
265266 die (_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE )
266267
267268# Mask the newly minted PyPI token, so that we don't accidentally leak it in logs.
268- print (f" ::add-mask::{ pypi_token } " , file = sys .stderr )
269+ print (f' ::add-mask::{ pypi_token } ' , file = sys .stderr )
269270
270271# This final print will be captured by the subshell in `twine-upload.sh`.
271272print (pypi_token )
0 commit comments