Skip to content

Commit 4cb8139

Browse files
authored
Merge pull request #1561 from gooddata/feature/slash-command-relay
feat(ci): add /fix slash command and widen review-relay filter
2 parents 740a411 + c915fc2 commit 4cb8139

2 files changed

Lines changed: 194 additions & 1 deletion

File tree

.github/workflows/sdk-review-relay.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
(github.event.review.state == 'changes_requested'
2424
|| (github.event.review.state == 'commented' && github.event.review.body))
2525
&& github.event.pull_request.user.login == 'yenkins-admin'
26-
&& startsWith(github.event.pull_request.head.ref, 'feature/auto-P')
26+
&& startsWith(github.event.pull_request.head.ref, 'auto/openapi-sync-')
2727
&& github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
2828
runs-on: ubuntu-latest
2929
timeout-minutes: 2
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# =============================================================================
2+
# SDK Slash Commands
3+
#
4+
# Minimal relay that forwards `/fix` PR comments to gdc-nas's review-fix
5+
# pipeline, using the same repository_dispatch payload as sdk-review-relay.
6+
#
7+
# SECURITY
8+
# --------
9+
# A PR comment can be authored by anyone (public repo), so access is gated by
10+
# whether the commenter is a collaborator on gooddata/gdc-nas. The check is
11+
# performed via the GitHub API using TOKEN_GITHUB_YENKINS_ADMIN (which already
12+
# has access to gdc-nas); no new secret is introduced.
13+
#
14+
# SCOPE
15+
# -----
16+
# Only reacts to comments on open PRs authored by yenkins-admin whose head
17+
# branch starts with `auto/openapi-sync-` (the sync pipeline's auto-branches).
18+
# Anything else is a silent no-op.
19+
#
20+
# CONCURRENCY NOTES
21+
# -----------------
22+
# - A second `/fix` on the same PR dispatches a second event. The receiver
23+
# (gdc-nas `sdk-py-review-fix.yml`) uses `cancel-in-progress: true` keyed
24+
# on PR number, so an earlier in-progress review-fix run will be cancelled
25+
# by a newer one. Acceptable at expected volume (single-digit/day).
26+
# - If a formal review (via sdk-review-relay.yml) and a `/fix` comment arrive
27+
# within the receiver's concurrency window, the receiver picks one and
28+
# cancels the other. Same mechanism.
29+
# =============================================================================
30+
name: SDK Slash Commands
31+
32+
on:
33+
issue_comment:
34+
types: [created]
35+
36+
# Default-deny for the whole workflow. Jobs must opt in to any scope they
37+
# actually need. Keeps future additions locked down by default.
38+
permissions: {}
39+
40+
concurrency:
41+
group: sdk-slash-fix-${{ github.event.issue.number }}
42+
cancel-in-progress: false
43+
44+
jobs:
45+
fix:
46+
name: "/fix — forward to gdc-nas"
47+
# Cheap gates first — anything that can be evaluated without an API call.
48+
# Job-level match is coarse (`startsWith(body, '/fix')`); the strict
49+
# "exact `/fix` token" check lives in the "Validate /fix syntax" step so
50+
# we can express the full regex.
51+
if: >-
52+
github.event.issue.pull_request != null
53+
&& github.event.issue.state == 'open'
54+
&& github.event.issue.user.login == 'yenkins-admin'
55+
&& startsWith(github.event.comment.body, '/fix')
56+
runs-on: ubuntu-latest
57+
timeout-minutes: 3
58+
permissions:
59+
# Only GITHUB_TOKEN use is `gh api repos/.../pulls/$PR_NUMBER` to read
60+
# head.ref. Reactions and dispatch both go through the PAT.
61+
pull-requests: read
62+
steps:
63+
- name: Validate /fix syntax
64+
id: cmd
65+
env:
66+
BODY: ${{ github.event.comment.body }}
67+
run: |
68+
# Strict: first line must be exactly `/fix` or `/fix` + whitespace.
69+
# Rejects `/fixme`, `/fix-review 42`, etc. Expanding the vocabulary
70+
# is an explicit non-goal (see plan §3).
71+
FIRST_LINE=$(printf '%s' "$BODY" | head -n1 | tr -d '\r')
72+
if [[ "$FIRST_LINE" =~ ^/fix([[:space:]].*)?$ ]]; then
73+
echo "matched=true" >> "$GITHUB_OUTPUT"
74+
else
75+
echo "First line '$FIRST_LINE' is not an exact /fix command — ignoring."
76+
echo "matched=false" >> "$GITHUB_OUTPUT"
77+
fi
78+
79+
- name: Resolve PR head branch
80+
id: pr
81+
if: steps.cmd.outputs.matched == 'true'
82+
env:
83+
GH_TOKEN: ${{ github.token }}
84+
PR_NUMBER: ${{ github.event.issue.number }}
85+
run: |
86+
HEAD_REF=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUMBER" \
87+
--jq '.head.ref')
88+
if [[ "$HEAD_REF" != auto/openapi-sync-* ]]; then
89+
echo "Branch '$HEAD_REF' is not an auto/openapi-sync-* branch — ignoring."
90+
echo "eligible=false" >> "$GITHUB_OUTPUT"
91+
else
92+
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
93+
echo "eligible=true" >> "$GITHUB_OUTPUT"
94+
fi
95+
96+
- name: Verify commenter has gdc-nas access
97+
id: auth
98+
if: steps.pr.outputs.eligible == 'true'
99+
env:
100+
# PAT scoped to read gdc-nas collaborator membership.
101+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
102+
USER: ${{ github.event.comment.user.login }}
103+
run: |
104+
if ! gh api "repos/gooddata/gdc-nas/collaborators/$USER" --silent 2>/dev/null; then
105+
# Silent reject: no reply, no reaction, and run stays green so the
106+
# public Actions tab does not leak who was denied.
107+
echo "::warning::User '$USER' is not a gdc-nas collaborator — /fix denied."
108+
echo "authorized=false" >> "$GITHUB_OUTPUT"
109+
exit 0
110+
fi
111+
echo "User '$USER' verified as gdc-nas collaborator."
112+
echo "authorized=true" >> "$GITHUB_OUTPUT"
113+
114+
- name: Parse /fix arguments
115+
id: parse
116+
if: steps.pr.outputs.eligible == 'true' && steps.auth.outputs.authorized == 'true'
117+
env:
118+
BODY: ${{ github.event.comment.body }}
119+
run: |
120+
# Take only the first line, strip CRLF, strip leading `/fix` +
121+
# whitespace, strip trailing whitespace. Remainder is review_body.
122+
FIRST_LINE=$(printf '%s' "$BODY" | head -n1 | tr -d '\r')
123+
ARGS=$(printf '%s' "$FIRST_LINE" | sed -E 's#^/fix[[:space:]]*##; s#[[:space:]]+$##')
124+
{
125+
echo "args<<EOF"
126+
printf '%s\n' "$ARGS"
127+
echo "EOF"
128+
} >> "$GITHUB_OUTPUT"
129+
130+
- name: Dispatch to gdc-nas
131+
id: dispatch
132+
if: steps.pr.outputs.eligible == 'true' && steps.auth.outputs.authorized == 'true'
133+
env:
134+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
135+
PR_NUMBER: ${{ github.event.issue.number }}
136+
HEAD_REF: ${{ steps.pr.outputs.head_ref }}
137+
COMMENTER: ${{ github.event.comment.user.login }}
138+
REVIEW_BODY: ${{ steps.parse.outputs.args }}
139+
run: |
140+
jq -nc \
141+
--arg pr_number "$PR_NUMBER" \
142+
--arg pr_branch "$HEAD_REF" \
143+
--arg pr_author "yenkins-admin" \
144+
--arg reviewer "$COMMENTER" \
145+
--arg review_id "" \
146+
--arg review_state "commented" \
147+
--arg review_body "$REVIEW_BODY" \
148+
'{
149+
event_type: "sdk-review-submitted",
150+
client_payload: {
151+
pr_number: $pr_number,
152+
pr_branch: $pr_branch,
153+
pr_author: $pr_author,
154+
reviewer: $reviewer,
155+
review_id: $review_id,
156+
review_state: $review_state,
157+
review_body: $review_body
158+
}
159+
}' | gh api "repos/gooddata/gdc-nas/dispatches" \
160+
--method POST \
161+
--input -
162+
163+
{
164+
echo "## /fix dispatched"
165+
echo "- PR: #$PR_NUMBER"
166+
echo "- Branch: \`$HEAD_REF\`"
167+
echo "- Commenter: @$COMMENTER"
168+
echo "- Review body: \`$REVIEW_BODY\`"
169+
} >> "$GITHUB_STEP_SUMMARY"
170+
171+
- name: Add rocket reaction (ack on successful dispatch)
172+
if: success() && steps.dispatch.outcome == 'success'
173+
env:
174+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
175+
COMMENT_ID: ${{ github.event.comment.id }}
176+
run: |
177+
gh api \
178+
--method POST \
179+
-H "Accept: application/vnd.github+json" \
180+
"repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" \
181+
-f content=rocket > /dev/null
182+
183+
- name: Add confused reaction (dispatch failed)
184+
if: failure() && steps.dispatch.outcome == 'failure'
185+
env:
186+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
187+
COMMENT_ID: ${{ github.event.comment.id }}
188+
run: |
189+
gh api \
190+
--method POST \
191+
-H "Accept: application/vnd.github+json" \
192+
"repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" \
193+
-f content=confused > /dev/null || true

0 commit comments

Comments
 (0)