Skip to content

Add project mascot icon to README #11

Add project mascot icon to README

Add project mascot icon to README #11

Workflow file for this run

name: Auto Review
on:
pull_request:
types: [opened, synchronize]
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