Skip to content

Commit 5937a75

Browse files
committed
feat(ci): add /fix slash command and widen review-relay filter
Relay now accepts both `auto/openapi-sync-*` and legacy `feature/auto-P*` PR branches, so reviews on the new sync pipeline's auto-PRs reach gdc-nas. Adds `sdk-slash-commands.yaml`: a `/fix` PR comment dispatches the same `sdk-review-submitted` repository_dispatch payload to gdc-nas that a formal review does. Access is gated by gdc-nas collaborator membership using the existing TOKEN_GITHUB_YENKINS_ADMIN PAT — no new secrets, no changes in gdc-nas. risk: low
1 parent 37d0593 commit 5937a75

2 files changed

Lines changed: 191 additions & 1 deletion

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ 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-')
27+
|| startsWith(github.event.pull_request.head.ref, 'feature/auto-P'))
2728
&& github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
2829
runs-on: ubuntu-latest
2930
timeout-minutes: 2
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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+
concurrency:
37+
group: sdk-slash-fix-${{ github.event.issue.number }}
38+
cancel-in-progress: false
39+
40+
jobs:
41+
fix:
42+
name: "/fix — forward to gdc-nas"
43+
# Cheap gates first — anything that can be evaluated without an API call.
44+
# Job-level match is coarse (`startsWith(body, '/fix')`); the strict
45+
# "exact `/fix` token" check lives in the "Validate /fix syntax" step so
46+
# we can express the full regex.
47+
if: >-
48+
github.event.issue.pull_request != null
49+
&& github.event.issue.state == 'open'
50+
&& github.event.issue.user.login == 'yenkins-admin'
51+
&& startsWith(github.event.comment.body, '/fix')
52+
runs-on: ubuntu-latest
53+
timeout-minutes: 3
54+
permissions:
55+
# Only GITHUB_TOKEN use is `gh api repos/.../pulls/$PR_NUMBER` to read
56+
# head.ref. Reactions and dispatch both go through the PAT.
57+
pull-requests: read
58+
steps:
59+
- name: Validate /fix syntax
60+
id: cmd
61+
env:
62+
BODY: ${{ github.event.comment.body }}
63+
run: |
64+
# Strict: first line must be exactly `/fix` or `/fix` + whitespace.
65+
# Rejects `/fixme`, `/fix-review 42`, etc. Expanding the vocabulary
66+
# is an explicit non-goal (see plan §3).
67+
FIRST_LINE=$(printf '%s' "$BODY" | head -n1 | tr -d '\r')
68+
if [[ "$FIRST_LINE" =~ ^/fix([[:space:]].*)?$ ]]; then
69+
echo "matched=true" >> "$GITHUB_OUTPUT"
70+
else
71+
echo "First line '$FIRST_LINE' is not an exact /fix command — ignoring."
72+
echo "matched=false" >> "$GITHUB_OUTPUT"
73+
fi
74+
75+
- name: Resolve PR head branch
76+
id: pr
77+
if: steps.cmd.outputs.matched == 'true'
78+
env:
79+
GH_TOKEN: ${{ github.token }}
80+
PR_NUMBER: ${{ github.event.issue.number }}
81+
run: |
82+
HEAD_REF=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUMBER" \
83+
--jq '.head.ref')
84+
if [[ "$HEAD_REF" != auto/openapi-sync-* ]]; then
85+
echo "Branch '$HEAD_REF' is not an auto/openapi-sync-* branch — ignoring."
86+
echo "eligible=false" >> "$GITHUB_OUTPUT"
87+
else
88+
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
89+
echo "eligible=true" >> "$GITHUB_OUTPUT"
90+
fi
91+
92+
- name: Verify commenter has gdc-nas access
93+
id: auth
94+
if: steps.pr.outputs.eligible == 'true'
95+
env:
96+
# PAT scoped to read gdc-nas collaborator membership.
97+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
98+
USER: ${{ github.event.comment.user.login }}
99+
run: |
100+
if ! gh api "repos/gooddata/gdc-nas/collaborators/$USER" --silent 2>/dev/null; then
101+
# Silent reject: no reply, no reaction, and run stays green so the
102+
# public Actions tab does not leak who was denied.
103+
echo "::warning::User '$USER' is not a gdc-nas collaborator — /fix denied."
104+
echo "authorized=false" >> "$GITHUB_OUTPUT"
105+
exit 0
106+
fi
107+
echo "User '$USER' verified as gdc-nas collaborator."
108+
echo "authorized=true" >> "$GITHUB_OUTPUT"
109+
110+
- name: Parse /fix arguments
111+
id: parse
112+
if: steps.pr.outputs.eligible == 'true' && steps.auth.outputs.authorized == 'true'
113+
env:
114+
BODY: ${{ github.event.comment.body }}
115+
run: |
116+
# Take only the first line, strip CRLF, strip leading `/fix` +
117+
# whitespace, strip trailing whitespace. Remainder is review_body.
118+
FIRST_LINE=$(printf '%s' "$BODY" | head -n1 | tr -d '\r')
119+
ARGS=$(printf '%s' "$FIRST_LINE" | sed -E 's#^/fix[[:space:]]*##; s#[[:space:]]+$##')
120+
{
121+
echo "args<<EOF"
122+
printf '%s\n' "$ARGS"
123+
echo "EOF"
124+
} >> "$GITHUB_OUTPUT"
125+
126+
- name: Dispatch to gdc-nas
127+
id: dispatch
128+
if: steps.pr.outputs.eligible == 'true' && steps.auth.outputs.authorized == 'true'
129+
env:
130+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
131+
PR_NUMBER: ${{ github.event.issue.number }}
132+
HEAD_REF: ${{ steps.pr.outputs.head_ref }}
133+
COMMENTER: ${{ github.event.comment.user.login }}
134+
REVIEW_BODY: ${{ steps.parse.outputs.args }}
135+
run: |
136+
jq -nc \
137+
--arg pr_number "$PR_NUMBER" \
138+
--arg pr_branch "$HEAD_REF" \
139+
--arg pr_author "yenkins-admin" \
140+
--arg reviewer "$COMMENTER" \
141+
--arg review_id "" \
142+
--arg review_state "commented" \
143+
--arg review_body "$REVIEW_BODY" \
144+
'{
145+
event_type: "sdk-review-submitted",
146+
client_payload: {
147+
pr_number: $pr_number,
148+
pr_branch: $pr_branch,
149+
pr_author: $pr_author,
150+
reviewer: $reviewer,
151+
review_id: $review_id,
152+
review_state: $review_state,
153+
review_body: $review_body
154+
}
155+
}' | gh api "repos/gooddata/gdc-nas/dispatches" \
156+
--method POST \
157+
--input -
158+
159+
{
160+
echo "## /fix dispatched"
161+
echo "- PR: #$PR_NUMBER"
162+
echo "- Branch: \`$HEAD_REF\`"
163+
echo "- Commenter: @$COMMENTER"
164+
echo "- Review body: \`$REVIEW_BODY\`"
165+
} >> "$GITHUB_STEP_SUMMARY"
166+
167+
- name: Add rocket reaction (ack on successful dispatch)
168+
if: success() && steps.dispatch.outcome == 'success'
169+
env:
170+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
171+
COMMENT_ID: ${{ github.event.comment.id }}
172+
run: |
173+
gh api \
174+
--method POST \
175+
-H "Accept: application/vnd.github+json" \
176+
"repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" \
177+
-f content=rocket > /dev/null
178+
179+
- name: Add confused reaction (dispatch failed)
180+
if: failure() && steps.dispatch.outcome == 'failure'
181+
env:
182+
GH_TOKEN: ${{ secrets.TOKEN_GITHUB_YENKINS_ADMIN }}
183+
COMMENT_ID: ${{ github.event.comment.id }}
184+
run: |
185+
gh api \
186+
--method POST \
187+
-H "Accept: application/vnd.github+json" \
188+
"repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" \
189+
-f content=confused > /dev/null || true

0 commit comments

Comments
 (0)