diff --git a/.github/workflows/auto-review.yml b/.github/workflows/auto-review.yml index fa69024..a6669da 100644 --- a/.github/workflows/auto-review.yml +++ b/.github/workflows/auto-review.yml @@ -4,6 +4,10 @@ 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 @@ -11,350 +15,8 @@ permissions: 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 <派单->修复->再审查->再派单) - 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 }}