Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 9 additions & 347 deletions .github/workflows/auto-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,357 +4,19 @@ on:
pull_request:
types: [opened, synchronize]

concurrency:
group: auto-review-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
pull-requests: write
contents: write
checks: read

jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Wait for CI checks to pass
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
echo "Waiting for CI checks on PR #${PR_NUMBER}..."

TIMEOUT=600
INTERVAL=15
ELAPSED=0

while [ $ELAPSED -lt $TIMEOUT ]; do
# 获取所有 check 状态,排除自己(Auto Review)
CHECKS=$(gh pr checks "$PR_NUMBER" --json name,state 2>/dev/null || echo "[]")

# 过滤掉自己这个 workflow 的 check
OTHER_CHECKS=$(echo "$CHECKS" | python3 -c "
import json, sys
checks = json.load(sys.stdin)
others = [c for c in checks if c['name'] != 'review']
if not others:
print('WAITING')
sys.exit(0)
states = [c['state'] for c in others]
if any(s == 'FAILURE' for s in states):
print('FAILURE')
elif all(s == 'SUCCESS' for s in states):
print('SUCCESS')
else:
print('WAITING')
")

if [ "$OTHER_CHECKS" = "SUCCESS" ]; then
echo "All CI checks passed"
break
elif [ "$OTHER_CHECKS" = "FAILURE" ]; then
echo "CI checks failed — skipping review"
exit 0
fi

echo " Checks still running... (${ELAPSED}s / ${TIMEOUT}s)"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done

if [ $ELAPSED -ge $TIMEOUT ]; then
echo "Timeout waiting for CI checks — skipping review"
exit 0
fi

- name: Determine review round
id: round
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
LABELS=$(gh pr view "$PR_NUMBER" --json labels -q '.labels[].name' 2>/dev/null || echo "")

# 找当前最大轮次
CURRENT_ROUND=0
for label in $LABELS; do
if echo "$label" | grep -qE '^review-round-[0-9]+$'; then
NUM=$(echo "$label" | sed 's/review-round-//')
if [ "$NUM" -gt "$CURRENT_ROUND" ]; then
CURRENT_ROUND=$NUM
fi
fi
done

NEXT_ROUND=$((CURRENT_ROUND + 1))
echo "round=$NEXT_ROUND" >> "$GITHUB_OUTPUT"
echo "Review round: $NEXT_ROUND"

- name: Run code review
id: review
env:
GH_TOKEN: ${{ github.token }}
CREW_API_TOKEN: ${{ secrets.CREW_API_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
REPO=${{ github.repository }}
ROUND=${{ steps.round.outputs.round }}

# 获取 PR diff — 写文件避免 shell 变量截断特殊字符
gh pr diff "$PR_NUMBER" | head -c 50000 > /tmp/pr_diff.txt

# 获取 PR 信息
PR_TITLE=$(gh pr view "$PR_NUMBER" --json title -q '.title')
PR_BODY=$(gh pr view "$PR_NUMBER" --json body -q '.body' | head -c 2000)

# 用 python3 从文件读 diff 构建 JSON payload,避免 shell 展开问题
PR_TITLE="$PR_TITLE" PR_BODY="$PR_BODY" REPO="$REPO" PR_NUMBER="$PR_NUMBER" ROUND="$ROUND" python3 << 'PYEOF' > /tmp/review_payload.json
import json, os

with open('/tmp/pr_diff.txt') as f:
diff = f.read()

repo = os.environ['REPO']
pr_number = os.environ['PR_NUMBER']
round_num = os.environ['ROUND']
title = os.environ['PR_TITLE']
body = os.environ['PR_BODY']

task = f"""审查 PR #{pr_number} 的代码变更。仓库: {repo}。这是第 {round_num} 轮审查。

PR 标题: {title}
PR 描述: {body}

代码变更 (diff):
{diff}

请以 JSON 格式返回审查结果,包含:
- approved: true/false
- summary: 审查总结
- comments: 具体意见数组(每条包含 file, line, comment)"""

print(json.dumps({'task': task, 'format': 'json', 'user_id': 'github-actions'}))
PYEOF

# 调用林锐审查(异步 API:POST 返回 task_id,轮询等结果)
SUBMIT=$(curl -s -X POST "https://crew.knowlyr.com/run/employee/code-reviewer" \
-H "Authorization: Bearer ${CREW_API_TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/review_payload.json)

TASK_ID=$(echo "$SUBMIT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('task_id',''))" 2>/dev/null)
if [ -z "$TASK_ID" ]; then
echo "Failed to submit review task: $SUBMIT — skipping review (not blocking PR)"
RESULT='{"approved": "skip", "summary": "Failed to submit review task", "comments": []}'
else
echo "Review task submitted: $TASK_ID — polling for result..."

# 轮询等待完成(最多 5 分钟)
POLL_TIMEOUT=300
POLL_INTERVAL=10
POLL_ELAPSED=0
RESPONSE=""

while [ $POLL_ELAPSED -lt $POLL_TIMEOUT ]; do
sleep $POLL_INTERVAL
POLL_ELAPSED=$((POLL_ELAPSED + POLL_INTERVAL))
POLL_RESP=$(curl -s "https://crew.knowlyr.com/tasks/${TASK_ID}?user_id=github-actions" \
-H "Authorization: Bearer ${CREW_API_TOKEN}")
STATUS=$(echo "$POLL_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null)

if [ "$STATUS" = "completed" ]; then
RESPONSE="$POLL_RESP"
echo "Review completed (${POLL_ELAPSED}s)"
break
elif [ "$STATUS" = "failed" ]; then
echo "Review task failed"
break
fi
echo " Still running... (${POLL_ELAPSED}s / ${POLL_TIMEOUT}s)"
done

if [ -z "$RESPONSE" ]; then
echo "Review task timed out or failed — skipping review (not blocking PR)"
RESULT='{"approved": "skip", "summary": "Review task timed out", "comments": []}'
else
# 解析返回结果:result.output 是林锐的文本输出,从中提取 JSON
# 逐位置尝试解析,找包含 approved key 的 JSON 对象(避免贪婪正则被文本中花括号干扰)
RESULT=$(echo "$RESPONSE" | python3 -c "
import json, sys

data = json.load(sys.stdin)
result = data.get('result', {})
text = result.get('output', '') if isinstance(result, dict) else str(result)

# 逐位置尝试解析 JSON,找包含 approved 的对象
parsed = None
for i, ch in enumerate(text):
if ch == '{':
for j in range(len(text), i, -1):
if text[j-1] == '}':
try:
candidate = json.loads(text[i:j])
if isinstance(candidate, dict) and 'approved' in candidate:
parsed = candidate
break
except (json.JSONDecodeError, ValueError):
continue
if parsed:
break

if parsed:
print(json.dumps(parsed))
else:
print(json.dumps({'approved': False, 'summary': text[:2000], 'comments': []}))
" 2>/dev/null || echo '{"approved": false, "summary": "Failed to parse review response", "comments": []}')
fi
fi

APPROVED=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('approved', False))")
SUMMARY=$(echo "$RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('summary', 'No summary'))")
COMMENTS=$(echo "$RESULT" | python3 -c "
import json, sys
data = json.load(sys.stdin)
comments = data.get('comments', [])
for c in comments:
f = c.get('file', '')
l = c.get('line', '')
msg = c.get('comment', '')
if f:
print(f'- **{f}**{(\" L\"+str(l)) if l else \"\"}: {msg}')
else:
print(f'- {msg}')
")

echo "approved=$APPROVED" >> "$GITHUB_OUTPUT"
echo "round=$ROUND" >> "$GITHUB_OUTPUT"

# 保存 summary 和 comments 到文件(避免多行变量问题)
echo "$SUMMARY" > /tmp/review_summary.txt
echo "$COMMENTS" > /tmp/review_comments.txt

# 清理临时文件
rm -f /tmp/pr_diff.txt /tmp/review_payload.json

- name: Apply review result
env:
GH_TOKEN: ${{ github.token }}
CREW_API_TOKEN: ${{ secrets.CREW_API_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
REPO=${{ github.repository }}
APPROVED="${{ steps.review.outputs.approved }}"
ROUND=${{ steps.review.outputs.round }}
SUMMARY=$(cat /tmp/review_summary.txt)
COMMENTS=$(cat /tmp/review_comments.txt)

# [Fix #2] CREW_API_TOKEN 校验
if [ -z "${CREW_API_TOKEN}" ]; then
echo "::warning::CREW_API_TOKEN not configured — skipping auto-dispatch"
fi

# 添加轮次 label
gh pr edit "$PR_NUMBER" --add-label "review-round-${ROUND}" 2>/dev/null || \
gh label create "review-round-${ROUND}" --color "0E8A16" 2>/dev/null && \
gh pr edit "$PR_NUMBER" --add-label "review-round-${ROUND}"

# 用 comment + exit code 控制 status check,不依赖 gh pr review --approve/--request-changes
# (GITHUB_TOKEN 无权 approve PR,--request-changes 会阻塞合并)
if [ "$APPROVED" = "skip" ]; then
echo "Review skipped (timeout or submit failure) — not blocking PR"
gh pr comment "$PR_NUMBER" --body "$(cat <<EOF
**Auto Review (Round ${ROUND}) — ⏭️ Skipped**

${SUMMARY}

审查任务超时或提交失败,不阻塞 PR。请人工确认是否需要审查。
EOF
)"
exit 0
elif [ "$APPROVED" = "True" ] || [ "$APPROVED" = "true" ]; then
echo "PR approved by code reviewer"
gh pr comment "$PR_NUMBER" --body "$(cat <<EOF
**Auto Review (Round ${ROUND}) — ✅ Approved**

${SUMMARY}
EOF
)"

# Auto merge — review 通过,自动合并
gh pr merge "$PR_NUMBER" --squash --auto

exit 0
else
echo "PR not approved — review has issues"
gh pr comment "$PR_NUMBER" --body "$(cat <<EOF
**Auto Review (Round ${ROUND}) — ❌ Changes Requested**

${SUMMARY}

${COMMENTS}
EOF
)"

# 审查不通过 — 只在第 1 轮自动派回修复(避免循环:审查->派单->修复->再审查->再派单)
if [ "$ROUND" -eq 1 ] && [ -n "${CREW_API_TOKEN}" ]; then
# [Fix #1] 用环境变量+文件方式传参,避免命令注入
PR_NUMBER="$PR_NUMBER" ROUND="$ROUND" REPO="$REPO" python3 << 'PYEOF' > /tmp/dispatch_payload.json
import json, os
summary = open('/tmp/review_summary.txt').read()
comments = open('/tmp/review_comments.txt').read()
task = f"""PR #{os.environ['PR_NUMBER']} 第 {os.environ['ROUND']} 轮审查不通过,需要派人修复。
仓库: {os.environ['REPO']}

审查意见:
{summary}

{comments}

请根据仓库和问题类型派合适的工程师修复,修完后推代码触发下一轮审查。"""
print(json.dumps({'task': task}))
PYEOF

# [Fix #3] dispatch curl 加 HTTP 状态码检查
DISPATCH_RESP=$(curl -s -w "\n%{http_code}" -X POST "https://crew.knowlyr.com/run/employee/ceo-assistant" \
-H "Authorization: Bearer ${CREW_API_TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/dispatch_payload.json)
HTTP_CODE=$(echo "$DISPATCH_RESP" | tail -n1)
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
echo "::warning::Auto-dispatch failed (HTTP $HTTP_CODE)"
else
echo "Auto-dispatch: review feedback sent to ceo-assistant"
fi
rm -f /tmp/dispatch_payload.json
fi

# 第 4 轮:需要人工介入
if [ "$ROUND" -ge 4 ]; then
echo "Round ${ROUND}: escalating to human review"
gh label create "needs-human-review" --color "D93F0B" 2>/dev/null || true
gh pr edit "$PR_NUMBER" --add-label "needs-human-review"

if [ -n "${CREW_API_TOKEN}" ]; then
# [Fix #1] 用环境变量+文件方式传参,避免命令注入
PR_NUMBER="$PR_NUMBER" ROUND="$ROUND" REPO="$REPO" python3 << 'PYEOF' > /tmp/escalate_payload.json
import json, os
task = f"PR #{os.environ['PR_NUMBER']} 经过 {os.environ['ROUND']} 轮审查仍未通过,需要 Kai 介入。仓库: {os.environ['REPO']}。请通过飞书通知 Kai。"
print(json.dumps({'task': task}))
PYEOF

# [Fix #3] dispatch curl 加 HTTP 状态码检查
ESCALATE_RESP=$(curl -s -w "\n%{http_code}" -X POST "https://crew.knowlyr.com/run/employee/ceo-assistant" \
-H "Authorization: Bearer ${CREW_API_TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/escalate_payload.json)
HTTP_CODE=$(echo "$ESCALATE_RESP" | tail -n1)
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
echo "::warning::Human review notification failed (HTTP $HTTP_CODE)"
else
echo "Human review notification sent"
fi
rm -f /tmp/escalate_payload.json
fi
fi

# 审查不通过 → exit 1 让 status check 变红,阻塞 PR 合并
exit 1
fi
uses: liuxiaotong/knowlyr-crew/.github/workflows/reusable-auto-review.yml@main
with:
dispatch_employee: "backend-engineer"
secrets:
CREW_API_TOKEN: ${{ secrets.CREW_API_TOKEN }}
Loading