Skip to content

Commit cd5a1e9

Browse files
authored
workflow: allow specific users to trigger backport comment commands (#4203)
Currently, the comment dispatch workflow only permits repository owners, members, and collaborators to run slash commands. Trusted contributors outside those roles cannot request backports on pull requests or release tracking issues via `/backport` comments. Add an explicit user allowlist to the workflow gate and enforce per-command authorization in the comment parser. Allowlisted users can trigger `/backport` commands on pull requests and release issues while remaining blocked from administrative release commands.
1 parent 28fe2e8 commit cd5a1e9

6 files changed

Lines changed: 257 additions & 61 deletions

File tree

‎.github/workflows/on_comment.py‎

Lines changed: 110 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Parses issue and PR comments to dispatch release and backport workflows."""
33

44
import argparse
5+
import enum
56
import json
67
import os
78
import re
@@ -101,83 +102,124 @@ def _react_negative(repo: str, comment_id: str) -> None:
101102
_add_comment_reaction(repo=repo, comment_id=comment_id, content="-1")
102103

103104

105+
class _Command(enum.StrEnum):
106+
"""Workflow commands dispatched from issue and PR comments."""
107+
108+
NONE = "none"
109+
CREATE_RC = "create-rc"
110+
PREPARE_COMPLETE = "prepare-complete"
111+
CREATE_RELEASE_BRANCH = "create-release-branch"
112+
PREPARE = "prepare"
113+
PROCESS_BACKPORTS = "process-backports"
114+
SYNC_CHANGELOG = "sync-changelog"
115+
ADD_BACKPORTS = "add-backports"
116+
PROMOTE = "promote"
117+
BACKPORT_PREPARE = "backport-prepare"
118+
BACKPORT_CREATE_RELEASES = "backport-create-releases"
119+
PR_BACKPORT = "pr-backport"
120+
121+
122+
_ALLOWED_AUTHOR_ASSOCIATIONS = frozenset({"OWNER", "MEMBER", "COLLABORATOR"})
123+
_BACKPORT_ALLOWED_USERS: frozenset[str] = frozenset([])
124+
125+
126+
def _is_command_allowed(
127+
command: _Command,
128+
*,
129+
user_login: str,
130+
author_association: str,
131+
) -> bool:
132+
"""Returns whether the user is allowed to trigger the given command."""
133+
if command in (_Command.ADD_BACKPORTS, _Command.PR_BACKPORT) and (
134+
user_login in _BACKPORT_ALLOWED_USERS
135+
):
136+
return True
137+
if author_association in _ALLOWED_AUTHOR_ASSOCIATIONS:
138+
return True
139+
return False
140+
141+
104142
def _process_release_issue_comment(
105143
comment_body: str,
106144
issue_number: str,
107145
repo: str = "",
108146
comment_id: str = "",
109-
) -> None:
147+
) -> _Command:
110148
"""Processes comments on a release tracking issue."""
111149
if _match_command("create-rc", comment_body):
112-
_write_github_output("command", "create-rc")
113-
return
150+
_write_github_output("command", _Command.CREATE_RC)
151+
return _Command.CREATE_RC
114152

115153
if m := _match_command("prepare-complete", comment_body):
116-
_write_github_output("command", "prepare-complete")
154+
_write_github_output("command", _Command.PREPARE_COMPLETE)
117155
if pr_arg := re.sub(r"[\s#]", "", m.group(1)) if m.group(1) else "":
118156
_write_github_output("pr_number", pr_arg)
119-
return
157+
return _Command.PREPARE_COMPLETE
120158

121159
if _match_command("create-release-branch", comment_body):
122-
_write_github_output("command", "create-release-branch")
123-
return
160+
_write_github_output("command", _Command.CREATE_RELEASE_BRANCH)
161+
return _Command.CREATE_RELEASE_BRANCH
124162

125163
if _match_command("prepare", comment_body):
126-
_write_github_output("command", "prepare")
127-
return
164+
_write_github_output("command", _Command.PREPARE)
165+
return _Command.PREPARE
128166

129167
if _match_command("process-backports", comment_body):
130-
_write_github_output("command", "process-backports")
131-
return
168+
_write_github_output("command", _Command.PROCESS_BACKPORTS)
169+
return _Command.PROCESS_BACKPORTS
132170

133171
if _match_command("sync-changelog", comment_body):
134-
_write_github_output("command", "sync-changelog")
135-
return
172+
_write_github_output("command", _Command.SYNC_CHANGELOG)
173+
return _Command.SYNC_CHANGELOG
136174

137175
if m := _match_command(("backport", "backports"), comment_body):
138176
raw_args = m.group(1) if m.group(1) else ""
139177
items = [item for item in re.split(r"[\s,]+", raw_args) if item]
140178
if csv := ",".join(items):
141-
_write_github_output("command", "add-backports")
179+
_write_github_output("command", _Command.ADD_BACKPORTS)
142180
_write_github_output("backports", csv)
181+
return _Command.ADD_BACKPORTS
143182
else:
144-
_write_github_output("command", "none")
183+
_write_github_output("command", _Command.NONE)
145184
_react_negative(repo=repo, comment_id=comment_id)
146-
return
185+
return _Command.NONE
147186

148187
if _match_command("promote", comment_body):
149-
_write_github_output("command", "promote")
150-
return
188+
_write_github_output("command", _Command.PROMOTE)
189+
return _Command.PROMOTE
151190

152-
_write_github_output("command", "none")
191+
_write_github_output("command", _Command.NONE)
192+
return _Command.NONE
153193

154194

155-
def _process_backport_issue_comment(comment_body: str) -> None:
195+
def _process_backport_issue_comment(comment_body: str) -> _Command:
156196
"""Processes comments on a backport tracking issue."""
157197
if _match_command("prepare", comment_body):
158-
_write_github_output("command", "backport-prepare")
159-
return
198+
_write_github_output("command", _Command.BACKPORT_PREPARE)
199+
return _Command.BACKPORT_PREPARE
160200

161201
if _match_command("create-releases", comment_body):
162-
_write_github_output("command", "backport-create-releases")
163-
return
202+
_write_github_output("command", _Command.BACKPORT_CREATE_RELEASES)
203+
return _Command.BACKPORT_CREATE_RELEASES
164204

165-
_write_github_output("command", "none")
205+
_write_github_output("command", _Command.NONE)
206+
return _Command.NONE
166207

167208

168-
def _process_pr_comment(comment_body: str, pr_number: str) -> None:
209+
def _process_pr_comment(comment_body: str, pr_number: str) -> _Command:
169210
"""Processes comments on a pull request."""
170211
if _match_command(("backport", "backports"), comment_body):
171-
_write_github_output("command", "pr-backport")
212+
_write_github_output("command", _Command.PR_BACKPORT)
172213
_write_github_output("pr_number", pr_number)
173-
return
214+
return _Command.PR_BACKPORT
174215

175216
if _match_command("prepare-complete", comment_body):
176-
_write_github_output("command", "prepare-complete")
217+
_write_github_output("command", _Command.PREPARE_COMPLETE)
177218
_write_github_output("pr_number", pr_number)
178-
return
219+
return _Command.PREPARE_COMPLETE
179220

180-
_write_github_output("command", "none")
221+
_write_github_output("command", _Command.NONE)
222+
return _Command.NONE
181223

182224

183225
def _report_failure() -> int:
@@ -205,7 +247,7 @@ def _report_failure() -> int:
205247
)
206248
return 1
207249

208-
if command and command != "none":
250+
if command and command != _Command.NONE:
209251
if comment_url:
210252
header = (
211253
f"Workflow failed for command `{command}` ([comment]({comment_url}))."
@@ -234,6 +276,12 @@ def process_comment(*, report_failure: bool = False) -> int:
234276
if report_failure:
235277
return _report_failure()
236278

279+
event = _load_event_data()
280+
comment_data = event.get("comment") or {}
281+
author_association = str(comment_data.get("author_association") or "")
282+
user_data = comment_data.get("user") or {}
283+
user_login = str(user_data.get("login") or "")
284+
237285
comment_body = os.environ.get("COMMENT_BODY", "")
238286
is_pr = _get_bool("IS_PR")
239287
event_number = os.environ.get("EVENT_NUMBER", "")
@@ -243,29 +291,40 @@ def process_comment(*, report_failure: bool = False) -> int:
243291
repo = os.environ.get("GITHUB_REPOSITORY", "")
244292

245293
if is_pr:
246-
_process_pr_comment(
294+
command = _process_pr_comment(
247295
comment_body=comment_body,
248296
pr_number=event_number,
249297
)
250-
return 0
251-
252-
issue_number = event_number
253-
_write_github_output("issue_number", issue_number)
254-
_write_github_env("issue_number", issue_number)
255-
256-
if has_release_label:
257-
_process_release_issue_comment(
258-
comment_body=comment_body,
259-
issue_number=issue_number,
260-
repo=repo,
261-
comment_id=comment_id,
262-
)
263-
elif has_backport_label:
264-
_process_backport_issue_comment(
265-
comment_body=comment_body,
266-
)
267298
else:
268-
_write_github_output("command", "none")
299+
issue_number = event_number
300+
_write_github_output("issue_number", issue_number)
301+
_write_github_env("issue_number", issue_number)
302+
303+
if has_release_label:
304+
command = _process_release_issue_comment(
305+
comment_body=comment_body,
306+
issue_number=issue_number,
307+
repo=repo,
308+
comment_id=comment_id,
309+
)
310+
elif has_backport_label:
311+
command = _process_backport_issue_comment(
312+
comment_body=comment_body,
313+
)
314+
else:
315+
_write_github_output("command", _Command.NONE)
316+
command = _Command.NONE
317+
318+
if command != _Command.NONE and not _is_command_allowed(
319+
command,
320+
user_login=user_login,
321+
author_association=author_association,
322+
):
323+
print(
324+
f"::error::User '{user_login}' (association: '{author_association}')"
325+
f" is not allowed to trigger command '{command}'."
326+
)
327+
return 1
269328

270329
return 0
271330

‎.github/workflows/on_comment.yaml‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ jobs:
1919

2020
parse_comment:
2121
runs-on: ubuntu-latest
22-
if: |
23-
github.event.comment.author_association == 'OWNER' ||
24-
github.event.comment.author_association == 'MEMBER' ||
25-
github.event.comment.author_association == 'COLLABORATOR'
22+
if: contains(github.event.comment.body, '/')
2623
outputs:
2724
command: ${{ steps.parse.outputs.command }}
2825
issue_number: ${{ steps.parse.outputs.issue_number }}

‎downloader_config.cfg‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Try GitHub first (primary)
2+
rewrite ^github\.com/bazel-contrib/bazel-gazelle/(.*) github.com/bazel-contrib/bazel-gazelle/$1
23
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1
34
rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1
45
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1
@@ -11,6 +12,7 @@ rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1
1112

1213
# Fall back to mirror (secondary)
1314
# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139
15+
rewrite ^github\.com/bazel-contrib/bazel-gazelle/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel-gazelle/$1
1416
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1
1517
rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1
1618
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1

‎gazelle/downloader_config.cfg‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Try GitHub first (primary)
2+
rewrite ^github\.com/bazel-contrib/bazel-gazelle/(.*) github.com/bazel-contrib/bazel-gazelle/$1
23
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1
34
rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1
45
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1
@@ -11,6 +12,7 @@ rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1
1112

1213
# Fall back to mirror (secondary)
1314
# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139
15+
rewrite ^github\.com/bazel-contrib/bazel-gazelle/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel-gazelle/$1
1416
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1
1517
rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1
1618
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1

‎sphinxdocs/downloader_config.cfg‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Try GitHub first (primary)
2+
rewrite ^github\.com/bazel-contrib/bazel-gazelle/(.*) github.com/bazel-contrib/bazel-gazelle/$1
23
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1
34
rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1
45
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1
@@ -11,6 +12,7 @@ rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1
1112

1213
# Fall back to mirror (secondary)
1314
# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139
15+
rewrite ^github\.com/bazel-contrib/bazel-gazelle/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel-gazelle/$1
1416
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1
1517
rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1
1618
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1

0 commit comments

Comments
 (0)