From 2cb21422555a8e7c28d12fd682c21ae3fb90396b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 23 Jul 2026 10:14:39 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(ci):=20=E6=8A=BD=E5=8F=96auto-approve?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=88=B0=E7=8B=AC=E7=AB=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci/auto_approve.sh | 151 +++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 scripts/ci/auto_approve.sh diff --git a/scripts/ci/auto_approve.sh b/scripts/ci/auto_approve.sh new file mode 100644 index 000000000..0c4f94bc8 --- /dev/null +++ b/scripts/ci/auto_approve.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# 自动审批:CI全绿后自动approve PR +# 环境变量:GITHUB_TOKEN, REVIEW_TOKEN, PR_NUMBER, PR_HEAD_SHA, GITHUB_API_URL, GITHUB_REPOSITORY +set -eu + +set -eu + +echo "PR #${PR_NUMBER} - 检查CI状态并自动审批" + +# 检查是否纯前端改动 +API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" +FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") +FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) +BACKEND_COUNT=$(echo "$FILES" | grep -cv '^apps/web/' || true) +TOTAL=$(echo "$FILES" | grep -cv '^$' || true) +echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" + +if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then + SKIP_BACKEND=true + echo "✅ 纯前端改动,只检查Frontend Lint" +else + SKIP_BACKEND=false + echo "🔧 包含后端/公共变更,检查全部CI" +fi + +# 定义需要检查的context +if [ "$SKIP_BACKEND" = "true" ]; then + CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") +else + CONTEXTS=( + "CI/CD Pipeline / Validate - Code Quality (pull_request)" + "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" + "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" + "CI/CD Pipeline / Frontend Lint (pull_request)" + ) +fi + +echo "需要通过的CI检查: ${#CONTEXTS[@]} 项(与分支保护required门禁一致)" +for ctx in "${CONTEXTS[@]}"; do + echo " - $ctx" +done +echo + +# 初始等待30秒,给CI启动写status的时间 +echo "等待30秒让CI启动..." +sleep 30 + +# 轮询等待,最多20分钟(120次x10秒) +for attempt in $(seq 1 12); do # 短作业模式:最多等2分钟(12次x10秒),不满足就退出等下次触发 + ALL_SUCCESS=true + ANY_FAILED=false + ANY_PENDING=false + + echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" + + # 调用辅助脚本检查每个context状态 + for ctx in "${CONTEXTS[@]}"; do + STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") + echo " $ctx: $STATE" + + if [ "$STATE" != "success" ]; then + ALL_SUCCESS=false + fi + if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then + ANY_FAILED=true + fi + if [ "$STATE" = "pending" ] || [ "$STATE" = "null" ]; then + ANY_PENDING=true + fi + done + + if [ "$ALL_SUCCESS" = "true" ]; then + echo + echo "✅ 所有CI检查通过,自动审批 PR #${PR_NUMBER}" + + # 检查是否已有审批 + EXISTING=$(curl -s -H "Authorization: token ${REVIEW_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \ + | python3 -c "import sys,json; reviews=json.load(sys.stdin); print('yes' if any(r.get('state')=='APPROVED' for r in reviews) else 'no')") + + if [ "$EXISTING" = "yes" ]; then + echo "ℹ️ PR #${PR_NUMBER} 已有审批,跳过" + exit 0 + fi + + # 第一步:创建PENDING review + echo "创建review..." + REVIEW_CREATE=$(curl -s -X POST \ + -H "Authorization: token ${REVIEW_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"event": "PENDING", "body": "CI全绿,自动审批通过。"}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews") + + REVIEW_ID=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))") + REVIEW_STATE=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") + echo "创建结果: id=$REVIEW_ID state=$REVIEW_STATE" + + if [ -z "$REVIEW_ID" ]; then + echo "❌ 创建review失败" + echo "$REVIEW_CREATE" + exit 1 + fi + + if [ "$REVIEW_STATE" = "APPROVED" ]; then + echo "✅ 自动审批成功(直接创建为APPROVED)" + exit 0 + fi + + # 第二步:submit review为APPROVED + echo "提交review审批..." + SUBMIT_CODE=$(curl -s -o /tmp/submit_resp.json -w "%{http_code}" \ + -X POST \ + -H "Authorization: token ${REVIEW_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"event": "APPROVED", "body": "CI全绿,自动审批通过。"}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews/${REVIEW_ID}") + + echo "提交API HTTP状态: $SUBMIT_CODE" + cat /tmp/submit_resp.json 2>/dev/null || true + echo + + if [ "$SUBMIT_CODE" = "200" ] || [ "$SUBMIT_CODE" = "201" ]; then + FINAL_STATE=$(python3 -c "import json; print(json.load(open('/tmp/submit_resp.json')).get('state',''))" 2>/dev/null || echo "?") + echo "✅ 自动审批成功 (state: $FINAL_STATE)" + exit 0 + else + echo "❌ 提交审批失败" + exit 1 + fi + fi + + # 还有CI在跑 → 继续等 + if [ "$ANY_PENDING" = "true" ]; then + echo "⏳ CI仍在运行中(第${attempt}/12次),超时后将退出等待下次触发..." + sleep 10 + continue + fi + + # 所有CI都跑完了但有失败 → 退出 + if [ "$ANY_FAILED" = "true" ]; then + echo + echo "❌ CI检查有失败项,不自动审批" + exit 0 + fi + + sleep 10 +done + +echo +echo "⏰ 快速检查超时(2分钟),CI尚未完成,退出等待下次触发(workflow_run事件或5分钟定时扫描)" +exit 0 -- 2.54.0 From d064c9921371b124181601142e84871fd0c7f39d Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 23 Jul 2026 10:14:40 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(ci):=20=E6=8A=BD=E5=8F=96auto-merge?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=88=B0=E7=8B=AC=E7=AB=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci/auto_merge.sh | 144 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 scripts/ci/auto_merge.sh diff --git a/scripts/ci/auto_merge.sh b/scripts/ci/auto_merge.sh new file mode 100644 index 000000000..3b36f3b74 --- /dev/null +++ b/scripts/ci/auto_merge.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# 自动合并:CI全绿+已审批后自动squash merge PR到develop +# 环境变量:GITHUB_TOKEN, MERGE_TOKEN, PR_NUMBER, PR_HEAD_SHA, BASE_REF, GITHUB_API_URL, GITHUB_REPOSITORY +set -eu + +set -eu + +echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" +echo + +# 只合develop分支 +if [ "$BASE_REF" != "develop" ]; then + echo "Skip: 目标分支不是develop" + exit 0 +fi + +# 判断是否纯前端改动 +FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" \ + | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") +TOTAL=$(echo "$FILES" | grep -cv '^$' || true) +FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) +BACKEND_COUNT=$((TOTAL - FRONTEND_COUNT)) +echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" + +if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then + CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") + echo "纯前端改动,只检查Frontend Lint" +else + CONTEXTS=( + "CI/CD Pipeline / Validate - Code Quality (pull_request)" + "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" + "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" + "CI/CD Pipeline / Frontend Lint (pull_request)" + ) + echo "检查required门禁(与分支保护一致)" +fi +echo + +# 初始等待30秒,给CI启动写status的时间 +echo "等待30秒让CI启动..." +sleep 30 + +# 405连续计数器 +MERGE_405_COUNT=0 +MAX_405_RETRIES=10 + +# 轮询等待,最多30分钟(180次x10秒) +for attempt in $(seq 1 90); do # 最多等45分钟(90次x30秒),确保等得到Worker构建完成 + ALL_SUCCESS=true + ANY_FAILED=false + ANY_PENDING=false + + echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" + + # 检查CI状态 + for ctx in "${CONTEXTS[@]}"; do + STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") + echo " CI: ${ctx##*/}: $STATE" + if [ "$STATE" != "success" ]; then + ALL_SUCCESS=false + fi + if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then + ANY_FAILED=true + fi + if [ "$STATE" = "pending" ]; then + ANY_PENDING=true + fi + done + + + # CI全绿 → 合并 + if [ "$ALL_SUCCESS" = "true" ]; then + echo + echo "CI全绿,执行自动合并" + echo "等待60秒冷却,给Gitea内部状态同步时间..." + sleep 60 + + # 幂等检查:PR是否还是open + PR_STATE=$(curl -s -H "Authorization: token ${MERGE_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") + + if [ "$PR_STATE" != "open" ]; then + echo "PR状态为 ${PR_STATE},无需合并" + exit 0 + fi + + # 执行squash merge + HTTP_CODE=$(curl -s -o /tmp/merge_resp.json -w "%{http_code}" \ + -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"do":"squash","merge_title_field":"","merge_message_field":"","delete_branch_after_merge":true,"force_merge":false}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/merge") + + echo "合并API HTTP状态: $HTTP_CODE" + + if [ "$HTTP_CODE" = "200" ]; then + echo "自动合并成功" + exit 0 + elif [ "$HTTP_CODE" = "405" ]; then + MERGE_405_COUNT=$((MERGE_405_COUNT + 1)) + echo "⚠️ 合并返回405(第${MERGE_405_COUNT}次),可能CI状态尚未同步或有未解决的门禁,继续等待重试..." + cat /tmp/merge_resp.json 2>/dev/null || true + echo + if [ "$MERGE_405_COUNT" -ge "$MAX_405_RETRIES" ]; then + echo "⚠️ 连续${MAX_405_RETRIES}次合并返回405,放弃自动合并(需人工确认,非代码问题)" + curl -s -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"body": "Auto merge skipped after multiple 405 errors: PR may have conflicts or unresolved checks. Please review manually. This is not a CI failure."}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true + exit 0 + fi + sleep 30 + continue + else + echo "自动合并失败 (HTTP $HTTP_CODE)" + cat /tmp/merge_resp.json 2>/dev/null || true + curl -s -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"body\": \"Auto merge failed (HTTP ${HTTP_CODE}), please check manually.\"}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true + exit 1 + fi + else + # 本轮不满足合并条件,重置405计数器 + MERGE_405_COUNT=0 + fi + + if [ "$ANY_FAILED" = "true" ]; then + echo + echo "CI有失败项,不自动合并" + exit 0 + fi + + sleep 30 +done + +echo +echo "快速检查超时(3分钟),CI尚未全绿或无审批,退出等待下次触发" +exit 0 -- 2.54.0 From daa4e80f53759a5ac37755e09bc94233a30a4e2c Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 23 Jul 2026 10:15:02 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(ci):=20auto-merge=E8=AF=AD=E6=B3=95bug?= =?UTF-8?q?=20-=20=E8=B0=83=E7=94=A8=E7=8B=AC=E7=AB=8B=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=E5=86=85=E5=B5=8C=E5=A4=A7=E6=AE=B5bash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/pr-automation.yml | 436 +++++------------------------ 1 file changed, 71 insertions(+), 365 deletions(-) diff --git a/.gitea/workflows/pr-automation.yml b/.gitea/workflows/pr-automation.yml index a239dfe94..a85d9d268 100755 --- a/.gitea/workflows/pr-automation.yml +++ b/.gitea/workflows/pr-automation.yml @@ -1,397 +1,103 @@ name: PR Automation - -on: +true: pull_request: - types: [synchronize, opened, ready_for_review, review_requested] - workflow_dispatch: - + types: + - synchronize + - opened + - ready_for_review + - review_requested + workflow_dispatch: null permissions: contents: read - jobs: auto-approve: name: Auto Approve on CI Green runs-on: ci-check if: github.event_name == 'pull_request' && !github.event.pull_request.draft - timeout-minutes: 3 # 长等待模式:等CI全绿后自动合并,不遗漏任何PR + timeout-minutes: 3 steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash + - name: Checkout code + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: 'curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash - - name: "🔍 脚本语法自检" - shell: bash - run: | - ERROR=0 - for f in scripts/ci/*.sh; do [ -f "$f" ] && bash -n "$f" 2>&1 || ERROR=$((ERROR+1)); done - for f in scripts/ci/*.py; do [ -f "$f" ] && python3 -m py_compile "$f" 2>&1 || ERROR=$((ERROR+1)); done - if [ "$ERROR" -ne 0 ]; then echo "❌ 语法自检失败 ($ERROR个)"; exit 1; fi - echo "✅ 脚本语法自检通过" + ' + - name: 🔍 脚本语法自检 + shell: bash + run: 'ERROR=0 - - name: Auto approve when CI passes - shell: bash - env: - GITHUB_TOKEN: ${{ github.token }} - REVIEW_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - set -eu + for f in scripts/ci/*.sh; do [ -f "$f" ] && bash -n "$f" 2>&1 || ERROR=$((ERROR+1)); done - echo "PR #${PR_NUMBER} - 检查CI状态并自动审批" + for f in scripts/ci/*.py; do [ -f "$f" ] && python3 -m py_compile "$f" 2>&1 || ERROR=$((ERROR+1)); done - # 检查是否纯前端改动 - API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" - FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") - FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) - BACKEND_COUNT=$(echo "$FILES" | grep -cv '^apps/web/' || true) - TOTAL=$(echo "$FILES" | grep -cv '^$' || true) - echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" + if [ "$ERROR" -ne 0 ]; then echo "❌ 语法自检失败 ($ERROR个)"; exit 1; fi - if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then - SKIP_BACKEND=true - echo "✅ 纯前端改动,只检查Frontend Lint" - else - SKIP_BACKEND=false - echo "🔧 包含后端/公共变更,检查全部CI" - fi + echo "✅ 脚本语法自检通过" - # 定义需要检查的context - if [ "$SKIP_BACKEND" = "true" ]; then - CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") - else - CONTEXTS=( - "CI/CD Pipeline / Validate - Code Quality (pull_request)" - "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" - "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" - "CI/CD Pipeline / Frontend Lint (pull_request)" - ) - fi + ' + - name: Auto approve when CI passes + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + REVIEW_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: bash scripts/ci/auto_approve.sh + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: 'STATUS="ok" - echo "需要通过的CI检查: ${#CONTEXTS[@]} 项(与分支保护required门禁一致)" - for ctx in "${CONTEXTS[@]}"; do - echo " - $ctx" - done - echo + [ ${{ job.status }} = "success" ] || STATUS="error" - # 初始等待30秒,给CI启动写status的时间 - echo "等待30秒让CI启动..." - sleep 30 + START_TIME="" - # 轮询等待,最多20分钟(120次x10秒) - for attempt in $(seq 1 12); do # 短作业模式:最多等2分钟(12次x10秒),不满足就退出等下次触发 - ALL_SUCCESS=true - ANY_FAILED=false - ANY_PENDING=false + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) - echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" - - # 调用辅助脚本检查每个context状态 - for ctx in "${CONTEXTS[@]}"; do - STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") - echo " $ctx: $STATE" - - if [ "$STATE" != "success" ]; then - ALL_SUCCESS=false - fi - if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then - ANY_FAILED=true - fi - if [ "$STATE" = "pending" ] || [ "$STATE" = "null" ]; then - ANY_PENDING=true - fi - done - - if [ "$ALL_SUCCESS" = "true" ]; then - echo - echo "✅ 所有CI检查通过,自动审批 PR #${PR_NUMBER}" - - # 检查是否已有审批 - EXISTING=$(curl -s -H "Authorization: token ${REVIEW_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \ - | python3 -c "import sys,json; reviews=json.load(sys.stdin); print('yes' if any(r.get('state')=='APPROVED' for r in reviews) else 'no')") - - if [ "$EXISTING" = "yes" ]; then - echo "ℹ️ PR #${PR_NUMBER} 已有审批,跳过" - exit 0 - fi - - # 第一步:创建PENDING review - echo "创建review..." - REVIEW_CREATE=$(curl -s -X POST \ - -H "Authorization: token ${REVIEW_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"event": "PENDING", "body": "CI全绿,自动审批通过。"}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews") - - REVIEW_ID=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))") - REVIEW_STATE=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") - echo "创建结果: id=$REVIEW_ID state=$REVIEW_STATE" - - if [ -z "$REVIEW_ID" ]; then - echo "❌ 创建review失败" - echo "$REVIEW_CREATE" - exit 1 - fi - - if [ "$REVIEW_STATE" = "APPROVED" ]; then - echo "✅ 自动审批成功(直接创建为APPROVED)" - exit 0 - fi - - # 第二步:submit review为APPROVED - echo "提交review审批..." - SUBMIT_CODE=$(curl -s -o /tmp/submit_resp.json -w "%{http_code}" \ - -X POST \ - -H "Authorization: token ${REVIEW_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"event": "APPROVED", "body": "CI全绿,自动审批通过。"}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews/${REVIEW_ID}") - - echo "提交API HTTP状态: $SUBMIT_CODE" - cat /tmp/submit_resp.json 2>/dev/null || true - echo - - if [ "$SUBMIT_CODE" = "200" ] || [ "$SUBMIT_CODE" = "201" ]; then - FINAL_STATE=$(python3 -c "import json; print(json.load(open('/tmp/submit_resp.json')).get('state',''))" 2>/dev/null || echo "?") - echo "✅ 自动审批成功 (state: $FINAL_STATE)" - exit 0 - else - echo "❌ 提交审批失败" - exit 1 - fi - fi - - # 还有CI在跑 → 继续等 - if [ "$ANY_PENDING" = "true" ]; then - echo "⏳ CI仍在运行中(第${attempt}/12次),超时后将退出等待下次触发..." - sleep 10 - continue - fi - - # 所有CI都跑完了但有失败 → 退出 - if [ "$ANY_FAILED" = "true" ]; then - echo - echo "❌ CI检查有失败项,不自动审批" - exit 0 - fi - - sleep 10 - done - - echo - echo "⏰ 快速检查超时(2分钟),CI尚未完成,退出等待下次触发(workflow_run事件或5分钟定时扫描)" - exit 0 - - - name: Report CI trace - if: always() - shell: sh - env: - AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} - run: | - STATUS="ok" - [ ${{ job.status }} = "success" ] || STATUS="error" - START_TIME="" - [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) - python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + ' auto-merge: name: Auto Merge on CI Green + Approved runs-on: ci-check if: github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.base.ref == 'develop' - timeout-minutes: 45 # 长等待模式:等CI全绿后自动合并,不遗漏任何PR + timeout-minutes: 45 steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash + - name: Checkout code + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: 'curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash - - name: "🔍 脚本语法自检(防止脚本bug导致所有PR挂掉)" - shell: bash - run: | - echo "=== CI脚本语法自检 ===" - ERROR=0 - for f in scripts/ci/*.sh; do - [ -f "$f" ] || continue - if ! bash -n "$f" 2>&1; then - echo "FAIL: $f" - ERROR=1 - fi - done - for f in scripts/ci/*.py; do - [ -f "$f" ] || continue - if ! python3 -m py_compile "$f" 2>&1; then - echo "FAIL: $f" - ERROR=1 - fi - done - if [ "$ERROR" -ne 0 ]; then - echo "❌ 脚本语法自检失败" - exit 1 - fi - echo "✅ 所有CI脚本语法自检通过" + ' + - name: 🔍 脚本语法自检(防止脚本bug导致所有PR挂掉) + shell: bash + run: "echo \"=== CI脚本语法自检 ===\"\nERROR=0\nfor f in scripts/ci/*.sh; do\n [ -f \"$f\" ] || continue\n if ! bash -n \"$f\" 2>&1; then\n echo \"FAIL: $f\"\n ERROR=1\n fi\ndone\nfor f in scripts/ci/*.py; do\n [ -f \"$f\" ] || continue\n if ! python3 -m py_compile \"$f\" 2>&1; then\n echo \"FAIL: $f\"\n ERROR=1\n fi\ndone\nif [ \"$ERROR\" -ne 0 ]; then\n echo \"❌ 脚本语法自检失败\"\n exit 1\nfi\necho \"✅ 所有CI脚本语法自检通过\"\n" + - name: Auto merge when CI passes and approved + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + MERGE_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: bash scripts/ci/auto_merge.sh + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: 'STATUS="ok" - - name: Auto merge when CI passes and approved - shell: bash - env: - GITHUB_TOKEN: ${{ github.token }} - MERGE_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - BASE_REF: ${{ github.event.pull_request.base.ref }} - run: | - set -eu + [ ${{ job.status }} = "success" ] || STATUS="error" - echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" - echo + START_TIME="" - # 只合develop分支 - if [ "$BASE_REF" != "develop" ]; then - echo "Skip: 目标分支不是develop" - exit 0 - fi + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) - # 判断是否纯前端改动 - FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" \ - | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") - TOTAL=$(echo "$FILES" | grep -cv '^$' || true) - FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) - BACKEND_COUNT=$((TOTAL - FRONTEND_COUNT)) - echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true - if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then - CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") - echo "纯前端改动,只检查Frontend Lint" - else - CONTEXTS=( - "CI/CD Pipeline / Validate - Code Quality (pull_request)" - "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" - "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" - "CI/CD Pipeline / Frontend Lint (pull_request)" - ) - echo "检查required门禁(与分支保护一致)" - fi - echo - - # 初始等待30秒,给CI启动写status的时间 - echo "等待30秒让CI启动..." - sleep 30 - - # 405连续计数器 - MERGE_405_COUNT=0 - MAX_405_RETRIES=10 - - # 轮询等待,最多30分钟(180次x10秒) - for attempt in $(seq 1 90); do # 最多等45分钟(90次x30秒),确保等得到Worker构建完成 - ALL_SUCCESS=true - ANY_FAILED=false - ANY_PENDING=false - - echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" - - # 检查CI状态 - for ctx in "${CONTEXTS[@]}"; do - STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") - echo " CI: ${ctx##*/}: $STATE" - if [ "$STATE" != "success" ]; then - ALL_SUCCESS=false - fi - if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then - ANY_FAILED=true - fi - if [ "$STATE" = "pending" ]; then - ANY_PENDING=true - fi - done - - - # CI全绿 → 合并 - if [ "$ALL_SUCCESS" = "true" ]; then - echo - echo "CI全绿,执行自动合并" - echo "等待60秒冷却,给Gitea内部状态同步时间..." - sleep 60 - - # 幂等检查:PR是否还是open - PR_STATE=$(curl -s -H "Authorization: token ${MERGE_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ - | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") - - if [ "$PR_STATE" != "open" ]; then - echo "PR状态为 ${PR_STATE},无需合并" - exit 0 - fi - - # 执行squash merge - HTTP_CODE=$(curl -s -o /tmp/merge_resp.json -w "%{http_code}" \ - -X POST \ - -H "Authorization: token ${MERGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"do":"squash","merge_title_field":"","merge_message_field":"","delete_branch_after_merge":true,"force_merge":false}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/merge") - - echo "合并API HTTP状态: $HTTP_CODE" - - if [ "$HTTP_CODE" = "200" ]; then - echo "自动合并成功" - exit 0 - elif [ "$HTTP_CODE" = "405" ]; then - MERGE_405_COUNT=$((MERGE_405_COUNT + 1)) - echo "⚠️ 合并返回405(第${MERGE_405_COUNT}次),可能CI状态尚未同步或有未解决的门禁,继续等待重试..." - cat /tmp/merge_resp.json 2>/dev/null || true - echo - if [ "$MERGE_405_COUNT" -ge "$MAX_405_RETRIES" ]; then - echo "⚠️ 连续${MAX_405_RETRIES}次合并返回405,放弃自动合并(需人工确认,非代码问题)" - curl -s -X POST \ - -H "Authorization: token ${MERGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"body": "Auto merge skipped after multiple 405 errors: PR may have conflicts or unresolved checks. Please review manually. This is not a CI failure."}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true - exit 0 - fi - sleep 30 - continue - else - echo "自动合并失败 (HTTP $HTTP_CODE)" - cat /tmp/merge_resp.json 2>/dev/null || true - curl -s -X POST \ - -H "Authorization: token ${MERGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"body\": \"Auto merge failed (HTTP ${HTTP_CODE}), please check manually.\"}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true - exit 1 - fi - else - # 本轮不满足合并条件,重置405计数器 - MERGE_405_COUNT=0 - fi - - if [ "$ANY_FAILED" = "true" ]; then - echo - echo "CI有失败项,不自动合并" - exit 0 - fi - - sleep 30 - done - - echo - echo "快速检查超时(3分钟),CI尚未全绿或无审批,退出等待下次触发" - exit 0 - - name: Report CI trace - if: always() - shell: sh - env: - AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} - run: | - STATUS="ok" - [ ${{ job.status }} = "success" ] || STATUS="error" - START_TIME="" - [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) - python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + ' -- 2.54.0 From c10eac868ab44c66fc08b07a426146a3042d7def Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 23 Jul 2026 10:21:56 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8DYAML=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=20-=20on=E8=A7=A6=E5=8F=91=E5=99=A8=E8=A2=ABPyYAML?= =?UTF-8?q?=E8=BD=AC=E4=B8=BAtrue=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/pr-automation.yml | 436 ++++++++++++++++++++++++----- 1 file changed, 365 insertions(+), 71 deletions(-) diff --git a/.gitea/workflows/pr-automation.yml b/.gitea/workflows/pr-automation.yml index a85d9d268..a239dfe94 100755 --- a/.gitea/workflows/pr-automation.yml +++ b/.gitea/workflows/pr-automation.yml @@ -1,103 +1,397 @@ name: PR Automation -true: + +on: pull_request: - types: - - synchronize - - opened - - ready_for_review - - review_requested - workflow_dispatch: null + types: [synchronize, opened, ready_for_review, review_requested] + workflow_dispatch: + permissions: contents: read + jobs: auto-approve: name: Auto Approve on CI Green runs-on: ci-check if: github.event_name == 'pull_request' && !github.event.pull_request.draft - timeout-minutes: 3 + timeout-minutes: 3 # 长等待模式:等CI全绿后自动合并,不遗漏任何PR steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: 'curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash + - name: Checkout code + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash - ' - - name: 🔍 脚本语法自检 - shell: bash - run: 'ERROR=0 + - name: "🔍 脚本语法自检" + shell: bash + run: | + ERROR=0 + for f in scripts/ci/*.sh; do [ -f "$f" ] && bash -n "$f" 2>&1 || ERROR=$((ERROR+1)); done + for f in scripts/ci/*.py; do [ -f "$f" ] && python3 -m py_compile "$f" 2>&1 || ERROR=$((ERROR+1)); done + if [ "$ERROR" -ne 0 ]; then echo "❌ 语法自检失败 ($ERROR个)"; exit 1; fi + echo "✅ 脚本语法自检通过" - for f in scripts/ci/*.sh; do [ -f "$f" ] && bash -n "$f" 2>&1 || ERROR=$((ERROR+1)); done + - name: Auto approve when CI passes + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + REVIEW_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -eu - for f in scripts/ci/*.py; do [ -f "$f" ] && python3 -m py_compile "$f" 2>&1 || ERROR=$((ERROR+1)); done + echo "PR #${PR_NUMBER} - 检查CI状态并自动审批" - if [ "$ERROR" -ne 0 ]; then echo "❌ 语法自检失败 ($ERROR个)"; exit 1; fi + # 检查是否纯前端改动 + API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" + FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") + FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) + BACKEND_COUNT=$(echo "$FILES" | grep -cv '^apps/web/' || true) + TOTAL=$(echo "$FILES" | grep -cv '^$' || true) + echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" - echo "✅ 脚本语法自检通过" + if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then + SKIP_BACKEND=true + echo "✅ 纯前端改动,只检查Frontend Lint" + else + SKIP_BACKEND=false + echo "🔧 包含后端/公共变更,检查全部CI" + fi - ' - - name: Auto approve when CI passes - shell: bash - env: - GITHUB_TOKEN: ${{ github.token }} - REVIEW_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: bash scripts/ci/auto_approve.sh - - name: Report CI trace - if: always() - shell: sh - env: - AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} - run: 'STATUS="ok" + # 定义需要检查的context + if [ "$SKIP_BACKEND" = "true" ]; then + CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") + else + CONTEXTS=( + "CI/CD Pipeline / Validate - Code Quality (pull_request)" + "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" + "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" + "CI/CD Pipeline / Frontend Lint (pull_request)" + ) + fi - [ ${{ job.status }} = "success" ] || STATUS="error" + echo "需要通过的CI检查: ${#CONTEXTS[@]} 项(与分支保护required门禁一致)" + for ctx in "${CONTEXTS[@]}"; do + echo " - $ctx" + done + echo - START_TIME="" + # 初始等待30秒,给CI启动写status的时间 + echo "等待30秒让CI启动..." + sleep 30 - [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + # 轮询等待,最多20分钟(120次x10秒) + for attempt in $(seq 1 12); do # 短作业模式:最多等2分钟(12次x10秒),不满足就退出等下次触发 + ALL_SUCCESS=true + ANY_FAILED=false + ANY_PENDING=false - python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" + + # 调用辅助脚本检查每个context状态 + for ctx in "${CONTEXTS[@]}"; do + STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") + echo " $ctx: $STATE" + + if [ "$STATE" != "success" ]; then + ALL_SUCCESS=false + fi + if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then + ANY_FAILED=true + fi + if [ "$STATE" = "pending" ] || [ "$STATE" = "null" ]; then + ANY_PENDING=true + fi + done + + if [ "$ALL_SUCCESS" = "true" ]; then + echo + echo "✅ 所有CI检查通过,自动审批 PR #${PR_NUMBER}" + + # 检查是否已有审批 + EXISTING=$(curl -s -H "Authorization: token ${REVIEW_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \ + | python3 -c "import sys,json; reviews=json.load(sys.stdin); print('yes' if any(r.get('state')=='APPROVED' for r in reviews) else 'no')") + + if [ "$EXISTING" = "yes" ]; then + echo "ℹ️ PR #${PR_NUMBER} 已有审批,跳过" + exit 0 + fi + + # 第一步:创建PENDING review + echo "创建review..." + REVIEW_CREATE=$(curl -s -X POST \ + -H "Authorization: token ${REVIEW_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"event": "PENDING", "body": "CI全绿,自动审批通过。"}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews") + + REVIEW_ID=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))") + REVIEW_STATE=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") + echo "创建结果: id=$REVIEW_ID state=$REVIEW_STATE" + + if [ -z "$REVIEW_ID" ]; then + echo "❌ 创建review失败" + echo "$REVIEW_CREATE" + exit 1 + fi + + if [ "$REVIEW_STATE" = "APPROVED" ]; then + echo "✅ 自动审批成功(直接创建为APPROVED)" + exit 0 + fi + + # 第二步:submit review为APPROVED + echo "提交review审批..." + SUBMIT_CODE=$(curl -s -o /tmp/submit_resp.json -w "%{http_code}" \ + -X POST \ + -H "Authorization: token ${REVIEW_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"event": "APPROVED", "body": "CI全绿,自动审批通过。"}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews/${REVIEW_ID}") + + echo "提交API HTTP状态: $SUBMIT_CODE" + cat /tmp/submit_resp.json 2>/dev/null || true + echo + + if [ "$SUBMIT_CODE" = "200" ] || [ "$SUBMIT_CODE" = "201" ]; then + FINAL_STATE=$(python3 -c "import json; print(json.load(open('/tmp/submit_resp.json')).get('state',''))" 2>/dev/null || echo "?") + echo "✅ 自动审批成功 (state: $FINAL_STATE)" + exit 0 + else + echo "❌ 提交审批失败" + exit 1 + fi + fi + + # 还有CI在跑 → 继续等 + if [ "$ANY_PENDING" = "true" ]; then + echo "⏳ CI仍在运行中(第${attempt}/12次),超时后将退出等待下次触发..." + sleep 10 + continue + fi + + # 所有CI都跑完了但有失败 → 退出 + if [ "$ANY_FAILED" = "true" ]; then + echo + echo "❌ CI检查有失败项,不自动审批" + exit 0 + fi + + sleep 10 + done + + echo + echo "⏰ 快速检查超时(2分钟),CI尚未完成,退出等待下次触发(workflow_run事件或5分钟定时扫描)" + exit 0 + + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true - ' auto-merge: name: Auto Merge on CI Green + Approved runs-on: ci-check if: github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.base.ref == 'develop' - timeout-minutes: 45 + timeout-minutes: 45 # 长等待模式:等CI全绿后自动合并,不遗漏任何PR steps: - - name: Checkout code - shell: sh - env: - GITHUB_TOKEN: ${{ github.token }} - run: 'curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash + - name: Checkout code + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash - ' - - name: 🔍 脚本语法自检(防止脚本bug导致所有PR挂掉) - shell: bash - run: "echo \"=== CI脚本语法自检 ===\"\nERROR=0\nfor f in scripts/ci/*.sh; do\n [ -f \"$f\" ] || continue\n if ! bash -n \"$f\" 2>&1; then\n echo \"FAIL: $f\"\n ERROR=1\n fi\ndone\nfor f in scripts/ci/*.py; do\n [ -f \"$f\" ] || continue\n if ! python3 -m py_compile \"$f\" 2>&1; then\n echo \"FAIL: $f\"\n ERROR=1\n fi\ndone\nif [ \"$ERROR\" -ne 0 ]; then\n echo \"❌ 脚本语法自检失败\"\n exit 1\nfi\necho \"✅ 所有CI脚本语法自检通过\"\n" - - name: Auto merge when CI passes and approved - shell: bash - env: - GITHUB_TOKEN: ${{ github.token }} - MERGE_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - BASE_REF: ${{ github.event.pull_request.base.ref }} - run: bash scripts/ci/auto_merge.sh - - name: Report CI trace - if: always() - shell: sh - env: - AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} - run: 'STATUS="ok" + - name: "🔍 脚本语法自检(防止脚本bug导致所有PR挂掉)" + shell: bash + run: | + echo "=== CI脚本语法自检 ===" + ERROR=0 + for f in scripts/ci/*.sh; do + [ -f "$f" ] || continue + if ! bash -n "$f" 2>&1; then + echo "FAIL: $f" + ERROR=1 + fi + done + for f in scripts/ci/*.py; do + [ -f "$f" ] || continue + if ! python3 -m py_compile "$f" 2>&1; then + echo "FAIL: $f" + ERROR=1 + fi + done + if [ "$ERROR" -ne 0 ]; then + echo "❌ 脚本语法自检失败" + exit 1 + fi + echo "✅ 所有CI脚本语法自检通过" - [ ${{ job.status }} = "success" ] || STATUS="error" + - name: Auto merge when CI passes and approved + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + MERGE_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -eu - START_TIME="" + echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" + echo - [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + # 只合develop分支 + if [ "$BASE_REF" != "develop" ]; then + echo "Skip: 目标分支不是develop" + exit 0 + fi - python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + # 判断是否纯前端改动 + FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" \ + | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") + TOTAL=$(echo "$FILES" | grep -cv '^$' || true) + FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) + BACKEND_COUNT=$((TOTAL - FRONTEND_COUNT)) + echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" - ' + if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then + CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") + echo "纯前端改动,只检查Frontend Lint" + else + CONTEXTS=( + "CI/CD Pipeline / Validate - Code Quality (pull_request)" + "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" + "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" + "CI/CD Pipeline / Frontend Lint (pull_request)" + ) + echo "检查required门禁(与分支保护一致)" + fi + echo + + # 初始等待30秒,给CI启动写status的时间 + echo "等待30秒让CI启动..." + sleep 30 + + # 405连续计数器 + MERGE_405_COUNT=0 + MAX_405_RETRIES=10 + + # 轮询等待,最多30分钟(180次x10秒) + for attempt in $(seq 1 90); do # 最多等45分钟(90次x30秒),确保等得到Worker构建完成 + ALL_SUCCESS=true + ANY_FAILED=false + ANY_PENDING=false + + echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" + + # 检查CI状态 + for ctx in "${CONTEXTS[@]}"; do + STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") + echo " CI: ${ctx##*/}: $STATE" + if [ "$STATE" != "success" ]; then + ALL_SUCCESS=false + fi + if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then + ANY_FAILED=true + fi + if [ "$STATE" = "pending" ]; then + ANY_PENDING=true + fi + done + + + # CI全绿 → 合并 + if [ "$ALL_SUCCESS" = "true" ]; then + echo + echo "CI全绿,执行自动合并" + echo "等待60秒冷却,给Gitea内部状态同步时间..." + sleep 60 + + # 幂等检查:PR是否还是open + PR_STATE=$(curl -s -H "Authorization: token ${MERGE_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") + + if [ "$PR_STATE" != "open" ]; then + echo "PR状态为 ${PR_STATE},无需合并" + exit 0 + fi + + # 执行squash merge + HTTP_CODE=$(curl -s -o /tmp/merge_resp.json -w "%{http_code}" \ + -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"do":"squash","merge_title_field":"","merge_message_field":"","delete_branch_after_merge":true,"force_merge":false}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/merge") + + echo "合并API HTTP状态: $HTTP_CODE" + + if [ "$HTTP_CODE" = "200" ]; then + echo "自动合并成功" + exit 0 + elif [ "$HTTP_CODE" = "405" ]; then + MERGE_405_COUNT=$((MERGE_405_COUNT + 1)) + echo "⚠️ 合并返回405(第${MERGE_405_COUNT}次),可能CI状态尚未同步或有未解决的门禁,继续等待重试..." + cat /tmp/merge_resp.json 2>/dev/null || true + echo + if [ "$MERGE_405_COUNT" -ge "$MAX_405_RETRIES" ]; then + echo "⚠️ 连续${MAX_405_RETRIES}次合并返回405,放弃自动合并(需人工确认,非代码问题)" + curl -s -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"body": "Auto merge skipped after multiple 405 errors: PR may have conflicts or unresolved checks. Please review manually. This is not a CI failure."}' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true + exit 0 + fi + sleep 30 + continue + else + echo "自动合并失败 (HTTP $HTTP_CODE)" + cat /tmp/merge_resp.json 2>/dev/null || true + curl -s -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"body\": \"Auto merge failed (HTTP ${HTTP_CODE}), please check manually.\"}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true + exit 1 + fi + else + # 本轮不满足合并条件,重置405计数器 + MERGE_405_COUNT=0 + fi + + if [ "$ANY_FAILED" = "true" ]; then + echo + echo "CI有失败项,不自动合并" + exit 0 + fi + + sleep 30 + done + + echo + echo "快速检查超时(3分钟),CI尚未全绿或无审批,退出等待下次触发" + exit 0 + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true -- 2.54.0 From ed45c25b9cf5e33cc07309116aef05f94d7a4579 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 23 Jul 2026 10:22:47 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(ci):=20auto-merge=E8=AF=AD=E6=B3=95bug?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20-=20=E6=8A=BD=E5=8F=96=E5=88=B0=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E8=84=9A=E6=9C=AC=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/pr-automation.yml | 288 +---------------------------- 1 file changed, 2 insertions(+), 286 deletions(-) diff --git a/.gitea/workflows/pr-automation.yml b/.gitea/workflows/pr-automation.yml index a239dfe94..663576e21 100755 --- a/.gitea/workflows/pr-automation.yml +++ b/.gitea/workflows/pr-automation.yml @@ -39,153 +39,7 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - set -eu - - echo "PR #${PR_NUMBER} - 检查CI状态并自动审批" - - # 检查是否纯前端改动 - API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" - FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") - FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) - BACKEND_COUNT=$(echo "$FILES" | grep -cv '^apps/web/' || true) - TOTAL=$(echo "$FILES" | grep -cv '^$' || true) - echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" - - if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then - SKIP_BACKEND=true - echo "✅ 纯前端改动,只检查Frontend Lint" - else - SKIP_BACKEND=false - echo "🔧 包含后端/公共变更,检查全部CI" - fi - - # 定义需要检查的context - if [ "$SKIP_BACKEND" = "true" ]; then - CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") - else - CONTEXTS=( - "CI/CD Pipeline / Validate - Code Quality (pull_request)" - "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" - "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" - "CI/CD Pipeline / Frontend Lint (pull_request)" - ) - fi - - echo "需要通过的CI检查: ${#CONTEXTS[@]} 项(与分支保护required门禁一致)" - for ctx in "${CONTEXTS[@]}"; do - echo " - $ctx" - done - echo - - # 初始等待30秒,给CI启动写status的时间 - echo "等待30秒让CI启动..." - sleep 30 - - # 轮询等待,最多20分钟(120次x10秒) - for attempt in $(seq 1 12); do # 短作业模式:最多等2分钟(12次x10秒),不满足就退出等下次触发 - ALL_SUCCESS=true - ANY_FAILED=false - ANY_PENDING=false - - echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" - - # 调用辅助脚本检查每个context状态 - for ctx in "${CONTEXTS[@]}"; do - STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") - echo " $ctx: $STATE" - - if [ "$STATE" != "success" ]; then - ALL_SUCCESS=false - fi - if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then - ANY_FAILED=true - fi - if [ "$STATE" = "pending" ] || [ "$STATE" = "null" ]; then - ANY_PENDING=true - fi - done - - if [ "$ALL_SUCCESS" = "true" ]; then - echo - echo "✅ 所有CI检查通过,自动审批 PR #${PR_NUMBER}" - - # 检查是否已有审批 - EXISTING=$(curl -s -H "Authorization: token ${REVIEW_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \ - | python3 -c "import sys,json; reviews=json.load(sys.stdin); print('yes' if any(r.get('state')=='APPROVED' for r in reviews) else 'no')") - - if [ "$EXISTING" = "yes" ]; then - echo "ℹ️ PR #${PR_NUMBER} 已有审批,跳过" - exit 0 - fi - - # 第一步:创建PENDING review - echo "创建review..." - REVIEW_CREATE=$(curl -s -X POST \ - -H "Authorization: token ${REVIEW_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"event": "PENDING", "body": "CI全绿,自动审批通过。"}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews") - - REVIEW_ID=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))") - REVIEW_STATE=$(echo "$REVIEW_CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") - echo "创建结果: id=$REVIEW_ID state=$REVIEW_STATE" - - if [ -z "$REVIEW_ID" ]; then - echo "❌ 创建review失败" - echo "$REVIEW_CREATE" - exit 1 - fi - - if [ "$REVIEW_STATE" = "APPROVED" ]; then - echo "✅ 自动审批成功(直接创建为APPROVED)" - exit 0 - fi - - # 第二步:submit review为APPROVED - echo "提交review审批..." - SUBMIT_CODE=$(curl -s -o /tmp/submit_resp.json -w "%{http_code}" \ - -X POST \ - -H "Authorization: token ${REVIEW_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"event": "APPROVED", "body": "CI全绿,自动审批通过。"}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews/${REVIEW_ID}") - - echo "提交API HTTP状态: $SUBMIT_CODE" - cat /tmp/submit_resp.json 2>/dev/null || true - echo - - if [ "$SUBMIT_CODE" = "200" ] || [ "$SUBMIT_CODE" = "201" ]; then - FINAL_STATE=$(python3 -c "import json; print(json.load(open('/tmp/submit_resp.json')).get('state',''))" 2>/dev/null || echo "?") - echo "✅ 自动审批成功 (state: $FINAL_STATE)" - exit 0 - else - echo "❌ 提交审批失败" - exit 1 - fi - fi - - # 还有CI在跑 → 继续等 - if [ "$ANY_PENDING" = "true" ]; then - echo "⏳ CI仍在运行中(第${attempt}/12次),超时后将退出等待下次触发..." - sleep 10 - continue - fi - - # 所有CI都跑完了但有失败 → 退出 - if [ "$ANY_FAILED" = "true" ]; then - echo - echo "❌ CI检查有失败项,不自动审批" - exit 0 - fi - - sleep 10 - done - - echo - echo "⏰ 快速检查超时(2分钟),CI尚未完成,退出等待下次触发(workflow_run事件或5分钟定时扫描)" - exit 0 - + bash scripts/ci/auto_approve.sh - name: Report CI trace if: always() shell: sh @@ -245,145 +99,7 @@ jobs: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} BASE_REF: ${{ github.event.pull_request.base.ref }} run: | - set -eu - - echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" - echo - - # 只合develop分支 - if [ "$BASE_REF" != "develop" ]; then - echo "Skip: 目标分支不是develop" - exit 0 - fi - - # 判断是否纯前端改动 - FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300" \ - | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin)]") - TOTAL=$(echo "$FILES" | grep -cv '^$' || true) - FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) - BACKEND_COUNT=$((TOTAL - FRONTEND_COUNT)) - echo "变更文件: ${TOTAL} 个 (前端: ${FRONTEND_COUNT}, 后端/公共: ${BACKEND_COUNT})" - - if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then - CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") - echo "纯前端改动,只检查Frontend Lint" - else - CONTEXTS=( - "CI/CD Pipeline / Validate - Code Quality (pull_request)" - "CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)" - "CI/CD Pipeline / Validate - Migration (alembic) (pull_request)" - "CI/CD Pipeline / Frontend Lint (pull_request)" - ) - echo "检查required门禁(与分支保护一致)" - fi - echo - - # 初始等待30秒,给CI启动写status的时间 - echo "等待30秒让CI启动..." - sleep 30 - - # 405连续计数器 - MERGE_405_COUNT=0 - MAX_405_RETRIES=10 - - # 轮询等待,最多30分钟(180次x10秒) - for attempt in $(seq 1 90); do # 最多等45分钟(90次x30秒),确保等得到Worker构建完成 - ALL_SUCCESS=true - ANY_FAILED=false - ANY_PENDING=false - - echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" - - # 检查CI状态 - for ctx in "${CONTEXTS[@]}"; do - STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") - echo " CI: ${ctx##*/}: $STATE" - if [ "$STATE" != "success" ]; then - ALL_SUCCESS=false - fi - if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then - ANY_FAILED=true - fi - if [ "$STATE" = "pending" ]; then - ANY_PENDING=true - fi - done - - - # CI全绿 → 合并 - if [ "$ALL_SUCCESS" = "true" ]; then - echo - echo "CI全绿,执行自动合并" - echo "等待60秒冷却,给Gitea内部状态同步时间..." - sleep 60 - - # 幂等检查:PR是否还是open - PR_STATE=$(curl -s -H "Authorization: token ${MERGE_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ - | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))") - - if [ "$PR_STATE" != "open" ]; then - echo "PR状态为 ${PR_STATE},无需合并" - exit 0 - fi - - # 执行squash merge - HTTP_CODE=$(curl -s -o /tmp/merge_resp.json -w "%{http_code}" \ - -X POST \ - -H "Authorization: token ${MERGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"do":"squash","merge_title_field":"","merge_message_field":"","delete_branch_after_merge":true,"force_merge":false}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/merge") - - echo "合并API HTTP状态: $HTTP_CODE" - - if [ "$HTTP_CODE" = "200" ]; then - echo "自动合并成功" - exit 0 - elif [ "$HTTP_CODE" = "405" ]; then - MERGE_405_COUNT=$((MERGE_405_COUNT + 1)) - echo "⚠️ 合并返回405(第${MERGE_405_COUNT}次),可能CI状态尚未同步或有未解决的门禁,继续等待重试..." - cat /tmp/merge_resp.json 2>/dev/null || true - echo - if [ "$MERGE_405_COUNT" -ge "$MAX_405_RETRIES" ]; then - echo "⚠️ 连续${MAX_405_RETRIES}次合并返回405,放弃自动合并(需人工确认,非代码问题)" - curl -s -X POST \ - -H "Authorization: token ${MERGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"body": "Auto merge skipped after multiple 405 errors: PR may have conflicts or unresolved checks. Please review manually. This is not a CI failure."}' \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true - exit 0 - fi - sleep 30 - continue - else - echo "自动合并失败 (HTTP $HTTP_CODE)" - cat /tmp/merge_resp.json 2>/dev/null || true - curl -s -X POST \ - -H "Authorization: token ${MERGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"body\": \"Auto merge failed (HTTP ${HTTP_CODE}), please check manually.\"}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true - exit 1 - fi - else - # 本轮不满足合并条件,重置405计数器 - MERGE_405_COUNT=0 - fi - - if [ "$ANY_FAILED" = "true" ]; then - echo - echo "CI有失败项,不自动合并" - exit 0 - fi - - sleep 30 - done - - echo - echo "快速检查超时(3分钟),CI尚未全绿或无审批,退出等待下次触发" - exit 0 + bash scripts/ci/auto_merge.sh - name: Report CI trace if: always() shell: sh -- 2.54.0