2
2
import json
3
3
import os
4
4
import sys
5
+ import typing as t
5
6
from http import HTTPStatus
6
7
from pathlib import Path
7
- from typing import NoReturn
8
8
from urllib .parse import urlparse
9
9
10
10
import id # pylint: disable=redefined-builtin
91
91
See https://docs.pypi.org/trusted-publishers/troubleshooting/ for more help.
92
92
"""
93
93
94
+ _REUSABLE_WORKFLOW_WARNING = """
95
+ The claims in this token suggest that the calling workflow is a reusable workflow.
96
+
97
+ In particular, this action was initiated by:
98
+
99
+ {job_workflow_ref}
100
+
101
+ Whereas its parent workflow is:
102
+
103
+ {workflow_ref}
104
+
105
+ Reusable workflows are **not currently supported** by PyPI's Trusted Publishing
106
+ functionality, and are subject to breakage. Users are **strongly encouraged**
107
+ to avoid using reusable workflows for Trusted Publishing until support
108
+ becomes official. Please, do not report bugs if this breaks.
109
+
110
+ For more information, see:
111
+
112
+ * https://docs.pypi.org/trusted-publishers/troubleshooting/#reusable-workflows-on-github
113
+ * https://github.com/pypa/gh-action-pypi-publish/issues/166 — subscribe to
114
+ this issue to watch the progress and learn when reusable workflows become
115
+ supported officially
116
+ """
117
+
94
118
# Rendered if the package index's token response isn't valid JSON.
95
119
_SERVER_TOKEN_RESPONSE_MALFORMED_JSON = """
96
120
Token request failed: the index produced an unexpected
111
135
""" # noqa: S105; not a password
112
136
113
137
114
- def die (msg : str ) -> NoReturn :
138
+ def die (msg : str ) -> t . NoReturn :
115
139
with _GITHUB_STEP_SUMMARY .open ('a' , encoding = 'utf-8' ) as io :
116
140
print (_ERROR_SUMMARY_MESSAGE .format (message = msg ), file = io )
117
141
@@ -123,6 +147,14 @@ def die(msg: str) -> NoReturn:
123
147
sys .exit (1 )
124
148
125
149
150
+ def warn (msg : str ) -> None :
151
+ with _GITHUB_STEP_SUMMARY .open ('a' , encoding = 'utf-8' ) as io :
152
+ print (msg , file = io )
153
+
154
+ msg = msg .replace ('\n ' , '%0A' )
155
+ print (f'::warning::Potential workflow misconfiguration: { msg } ' , file = sys .stderr )
156
+
157
+
126
158
def debug (msg : str ):
127
159
print (f'::debug::{ msg .title ()} ' , file = sys .stderr )
128
160
@@ -162,13 +194,15 @@ def assert_successful_audience_call(resp: requests.Response, domain: str):
162
194
)
163
195
164
196
165
- def render_claims (token : str ) -> str :
197
+ def extract_claims (token : str ) -> dict [ str , object ] :
166
198
_ , payload , _ = token .split ('.' , 2 )
167
199
168
200
# urlsafe_b64decode needs padding; JWT payloads don't contain any.
169
201
payload += '=' * (4 - (len (payload ) % 4 ))
170
- claims = json .loads (base64 .urlsafe_b64decode (payload ))
202
+ return json .loads (base64 .urlsafe_b64decode (payload ))
171
203
204
+
205
+ def render_claims (claims : dict [str , object ]) -> str :
172
206
def _get (name : str ) -> str : # noqa: WPS430
173
207
return claims .get (name , 'MISSING' )
174
208
@@ -184,6 +218,19 @@ def _get(name: str) -> str: # noqa: WPS430
184
218
)
185
219
186
220
221
+ def warn_on_reusable_workflow (claims : dict [str , object ]) -> None :
222
+ # A reusable workflow is identified by having different values
223
+ # for its workflow_ref (the initiating workflow) and job_workflow_ref
224
+ # (the reusable workflow).
225
+ workflow_ref = claims .get ('workflow_ref' )
226
+ job_workflow_ref = claims .get ('job_workflow_ref' )
227
+
228
+ if workflow_ref == job_workflow_ref :
229
+ return
230
+
231
+ warn (_REUSABLE_WORKFLOW_WARNING .format_map (locals ()))
232
+
233
+
187
234
def event_is_third_party_pr () -> bool :
188
235
# Non-`pull_request` events cannot be from third-party PRs.
189
236
if os .getenv ('GITHUB_EVENT_NAME' ) != 'pull_request' :
@@ -225,12 +272,19 @@ def event_is_third_party_pr() -> bool:
225
272
oidc_token = id .detect_credential (audience = oidc_audience )
226
273
except id .IdentityError as identity_error :
227
274
cause_msg_tmpl = (
228
- _TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE if event_is_third_party_pr ()
275
+ _TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE
276
+ if event_is_third_party_pr ()
229
277
else _TOKEN_RETRIEVAL_FAILED_MESSAGE
230
278
)
231
279
for_cause_msg = cause_msg_tmpl .format (identity_error = identity_error )
232
280
die (for_cause_msg )
233
281
282
+
283
+ # Perform a non-fatal check to see if we're running on a reusable
284
+ # workflow, and emit a warning if so.
285
+ oidc_claims = extract_claims (oidc_token )
286
+ warn_on_reusable_workflow (oidc_claims )
287
+
234
288
# Now we can do the actual token exchange.
235
289
mint_token_resp = requests .post (
236
290
token_exchange_url ,
@@ -257,7 +311,7 @@ def event_is_third_party_pr() -> bool:
257
311
for error in mint_token_payload ['errors' ]
258
312
)
259
313
260
- rendered_claims = render_claims (oidc_token )
314
+ rendered_claims = render_claims (oidc_claims )
261
315
262
316
die (
263
317
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE .format (
0 commit comments