2424 CLAUDE_CODE_OAUTH_TOKEN : ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
2525 if : >
2626 github.event.action == 'opened' &&
27- !startsWith(github.head_ref, 'mergify/')
27+ !startsWith(github.head_ref, 'mergify/') &&
28+ !contains(github.head_ref, '-sync-pr-')
2829 steps :
30+ - name : Compute skip rule
31+ id : gate
32+ env :
33+ PR : ${{ github.event.number }}
34+ REPO : ${{ github.repository }}
35+ TITLE : ${{ github.event.pull_request.title }}
36+ run : |
37+ set -x
38+ # Skip AI reviewer assignment for small/test-only PRs to reduce noise.
39+ # Adjust BUGFIX_LINE_THRESHOLD as needed (non-test add+del lines).
40+ BUGFIX_LINE_THRESHOLD=30
41+ TEST_PATTERN='(Test\.(java|kt)$|_test\.(cpp|cc|h)$|/test/|^test/|^fe-test/|^regression-test/|/__test__/)'
42+
43+ skip=false
44+ reason=""
45+
46+ # Rule 1: [UT] / [Doc] title prefix
47+ if echo "$TITLE" | grep -qE '^\[(UT|Doc)\]'; then
48+ skip=true
49+ reason="title prefix [UT] or [Doc]"
50+ fi
51+
52+ # Rule 2: all changed files match test pattern
53+ if [ "$skip" = "false" ]; then
54+ files=$(gh pr diff "$PR" -R "$REPO" --name-only 2>/dev/null || true)
55+ total=$(echo "$files" | grep -cv '^$' || true)
56+ non_test=$(echo "$files" | grep -v '^$' | grep -cvE "$TEST_PATTERN" || true)
57+ if [ "$total" -gt 0 ] && [ "$non_test" -eq 0 ]; then
58+ skip=true
59+ reason="all $total changed files are tests"
60+ fi
61+ fi
62+
63+ # Rule 3: [BugFix] with non-test add+del < threshold
64+ # NOTE: jq's regex needs \\. for a literal dot (the pattern must match TEST_PATTERN above).
65+ if [ "$skip" = "false" ] && echo "$TITLE" | grep -qE '^\[BugFix\]'; then
66+ non_test_lines=$(gh pr view "$PR" -R "$REPO" --json files --jq '
67+ [.files[]
68+ | select(.path | test("(Test\\.(java|kt)$|_test\\.(cpp|cc|h)$|/test/|^test/|^fe-test/|^regression-test/|/__test__/)") | not)
69+ | (.additions + .deletions)]
70+ | add // 0
71+ ' 2>/dev/null || echo 0)
72+ if [ "${non_test_lines:-0}" -lt "$BUGFIX_LINE_THRESHOLD" ]; then
73+ skip=true
74+ reason="[BugFix] non-test lines=$non_test_lines < $BUGFIX_LINE_THRESHOLD"
75+ fi
76+ fi
77+
78+ echo "skip=$skip" >> "$GITHUB_OUTPUT"
79+ echo "reason=$reason" >> "$GITHUB_OUTPUT"
80+ echo "=== gate result: skip=$skip, reason='$reason' ==="
81+
82+ - name : Sync skills to latest
83+ if : steps.gate.outputs.skip != 'true'
84+ run : |
85+ set -e
86+ SKILL_REPO="$HOME/starrocks-skills"
87+ SKILL_DEST="$HOME/.claude/skills"
88+
89+ if [ -d "$SKILL_REPO/.git" ]; then
90+ cd "$SKILL_REPO" && git pull --ff-only --quiet \
91+ || echo "WARN: git pull failed, using current checkout"
92+ else
93+ echo "WARN: $SKILL_REPO is not a git checkout, skip pull"
94+ fi
95+
96+ mkdir -p "$SKILL_DEST"
97+ for skill_dir in "$SKILL_REPO"/*/; do
98+ [ -d "$skill_dir" ] || continue
99+ name=$(basename "$skill_dir")
100+ rsync -a --delete "$skill_dir" "$SKILL_DEST/$name/"
101+ done
102+
103+ echo "Skills synced to $SKILL_DEST:"
104+ ls -1 "$SKILL_DEST"
105+
29106 - name : Assign PR Reviewer
107+ if : steps.gate.outputs.skip != 'true'
30108 run : |
31109 echo "=== DEBUG ==="
32110 echo "PR: ${{ github.event.number }}"
@@ -36,8 +114,66 @@ jobs:
36114 echo "CLAUDE_CODE_OAUTH_TOKEN length: ${#CLAUDE_CODE_OAUTH_TOKEN}"
37115 echo "GITHUB_TOKEN length: ${#GITHUB_TOKEN}"
38116 echo "=== RUN ==="
117+ # tee captures the skill's stdout for the Notify Slack step to parse
39118 docker exec -i -u claudeuser \
40119 -e GITHUB_TOKEN="${GITHUB_TOKEN}" \
41120 -e CLAUDE_CODE_OAUTH_TOKEN="${CLAUDE_CODE_OAUTH_TOKEN}" \
42121 claude-cli claude --dangerously-skip-permissions \
43- -p "/starrocks-assign-pr-reviewer https://github.com/${{ github.repository }}/pull/${{ github.event.number }}" || true
122+ -p "/starrocks-assign-pr-reviewer https://github.com/${{ github.repository }}/pull/${{ github.event.number }}" \
123+ 2>&1 | tee "${RUNNER_TEMP}/ai_output.txt" || true
124+
125+ - name : Notify Slack
126+ if : steps.gate.outputs.skip != 'true'
127+ env :
128+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
129+ SLACK_BOT_TOKEN : ${{ secrets.SLACK_BOT_TOKEN }}
130+ PR : ${{ github.event.number }}
131+ REPO : ${{ github.repository }}
132+ TITLE : ${{ github.event.pull_request.title }}
133+ PR_URL : ${{ github.event.pull_request.html_url }}
134+ CHANNEL_ID : C0B4GU36HJN
135+ run : |
136+ set -e
137+ rm -rf ./ci-tool && cp -rf /var/lib/ci-tool ./ci-tool
138+ cd ci-tool && git pull
139+
140+ # Parse the primary reviewer from the skill's output.
141+ # The skill prints: "Recommended reviewer: <login>"
142+ # (see starrocks-skills/starrocks-assign-pr-reviewer/SKILL.md "Output Template").
143+ # The LLM often wraps the line/value in markdown bold or italics
144+ # (e.g. **Recommended reviewer: kevincai**, Recommended reviewer: **kevincai**).
145+ # Tolerate optional leading *, then extract only valid GitHub login chars
146+ # ([A-Za-z0-9-]) to drop @, **, backticks, etc.
147+ ai_output="${RUNNER_TEMP}/ai_output.txt"
148+ reviewers=""
149+ if [ -f "$ai_output" ]; then
150+ reviewers=$(grep -iE '^\**Recommended reviewer:' "$ai_output" | tail -1 \
151+ | sed -E 's/^\**[Rr]ecommended reviewer:\**[[:space:]]*//' \
152+ | grep -oE '[A-Za-z0-9][A-Za-z0-9-]*' | head -1 || true)
153+ fi
154+
155+ if [ -z "$reviewers" ]; then
156+ echo "Could not extract AI-assigned reviewers from output, skip Slack notification"
157+ exit 0
158+ fi
159+ echo "AI assigned: $(echo "$reviewers" | tr '\n' ' ')"
160+
161+ # GitHub login -> Slack ID; unmapped users fall back to plain @login
162+ mentions=""
163+ while IFS= read -r login; do
164+ [ -z "$login" ] && continue
165+ slack_id=$(jq -r --arg k "$login" '.[$k] // empty' conf/member_slack.json)
166+ if [ -n "$slack_id" ]; then
167+ mentions="${mentions}<@${slack_id}> "
168+ else
169+ mentions="${mentions}@${login} "
170+ fi
171+ done <<< "$reviewers"
172+
173+ text="${mentions}You were assigned as reviewer for <${PR_URL}|#${PR} ${TITLE}>"
174+
175+ python3 lib/slack_notify.py \
176+ --mode channel \
177+ --token "$SLACK_BOT_TOKEN" \
178+ --channel "$CHANNEL_ID" \
179+ --text "$text"
0 commit comments