Add project mascot icon to README #11
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |