From 9a1483b52ef36339c437e7cf9276e27f987df028 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 11:20:24 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(ci):=20auto-merge=E4=BB=8E=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E8=A7=A6=E5=8F=91=E6=94=B9=E4=B8=BAPR=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E5=8D=B3=E6=97=B6=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 触发方式:从cron定时改为pull_request事件触发 - 合并条件:三门禁全绿 + 至少1个APPROVED + 无冲突 + 非草稿 + base=develop - 安全措施:幂等保护、合并失败留评论不重试、只合develop - 从6小时延迟改为秒级自动合并 --- .gitea/workflows/auto-merge.yml | 172 ++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 .gitea/workflows/auto-merge.yml diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml new file mode 100644 index 000000000..0f388ad8e --- /dev/null +++ b/.gitea/workflows/auto-merge.yml @@ -0,0 +1,172 @@ +name: Auto Merge CI PRs + +on: + pull_request: + types: [synchronize, opened, ready_for_review, review_requested, submitted] + +jobs: + auto-merge: + name: Auto Merge on CI Green + Approved + runs-on: ci-l1 + if: github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.base.ref == 'develop' + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - 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 }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -eu + + echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" + echo "base: ${BASE_REF}" + echo + + # 只合develop分支 + if [ "$BASE_REF" != "develop" ]; then + echo "ℹ️ 目标分支不是develop,跳过自动合并" + exit 0 + 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})" + + 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 And Tests (pull_request)" + "CI/CD Pipeline / Unit Tests (pull_request)" + "CI/CD Pipeline / Frontend Lint (pull_request)" + ) + fi + + echo "需要通过的CI检查: ${#CONTEXTS[@]} 项" + for ctx in "${CONTEXTS[@]}"; do + echo " - $ctx" + done + echo + + # 轮询等待CI + 审批,最多30分钟(180次x10秒) + for attempt in $(seq 1 180); do + ALL_SUCCESS=true + ANY_FAILED=false + + echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" + + # 检查每个context状态 + for ctx in "${CONTEXTS[@]}"; do + STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$ctx") + echo " CI: $ctx: $STATE" + + if [ "$STATE" != "success" ]; then + ALL_SUCCESS=false + fi + if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then + ANY_FAILED=true + fi + done + + # 检查审批状态 + REVIEW_COUNT=$(curl -s -H "Authorization: token ${MERGE_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \ + | python3 -c " +import sys, json +reviews = json.load(sys.stdin) +# 统计APPROVED的人数(去重) +approvers = set() +for r in reviews: + if r.get('state') == 'APPROVED': + approvers.add(r.get('user', {}).get('login', '')) +print(len(approvers)) +") + echo " 审批: ${REVIEW_COUNT} 个 APPROVED" + HAS_APPROVAL=false + if [ "$REVIEW_COUNT" -ge 1 ]; then + HAS_APPROVAL=true + fi + + # 全部满足 → 合并 + if [ "$ALL_SUCCESS" = "true" ] && [ "$HAS_APPROVAL" = "true" ]; then + echo + echo "✅ CI全绿 + 审批通过,自动合并 PR #${PR_NUMBER}" + + # 幂等检查:确认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_NUMBER} 状态为 ${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 + echo "⚠️ 合并失败(405 Method Not Allowed),可能是有冲突或门禁未通过" + # 留评论提醒 + curl -s -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"body\": \"🤖 自动合并失败:PR可能有冲突或门禁未全部通过,请人工检查。\"}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true + echo "ℹ️ 已在PR留言提醒" + exit 0 + else + echo "❌ 自动合并失败 (HTTP $HTTP_CODE)" + cat /tmp/merge_resp.json 2>/dev/null || true + echo + # 留评论提醒 + curl -s -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"body\": \"🤖 自动合并失败(HTTP ${HTTP_CODE}),请人工检查。\"}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true + exit 1 + fi + fi + + if [ "$ANY_FAILED" = "true" ]; then + echo + echo "❌ CI检查有失败项,不自动合并" + exit 0 + fi + + sleep 10 + done + + echo + echo "⏰ 等待超时(30分钟),CI或审批尚未满足条件" + exit 0 -- 2.54.0 From 0593e8e67e4a215778ec7ddc460950a01b13d193 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 11:31:38 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8Dauto-merge=20Y?= =?UTF-8?q?AML=E8=AF=AD=E6=B3=95=20-=20=E5=86=85=E8=81=94=E5=A4=9A?= =?UTF-8?q?=E8=A1=8CPython=E5=AF=BC=E8=87=B4=E8=A7=A3=E6=9E=90=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改用独立脚本scripts/check_pr_approval.py检查审批状态,避免YAML块标量多行缩进问题 --- .gitea/workflows/auto-merge.yml | 80 +++++++++++---------------------- 1 file changed, 26 insertions(+), 54 deletions(-) diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml index 0f388ad8e..1b95641fd 100644 --- a/.gitea/workflows/auto-merge.yml +++ b/.gitea/workflows/auto-merge.yml @@ -2,7 +2,7 @@ name: Auto Merge CI PRs on: pull_request: - types: [synchronize, opened, ready_for_review, review_requested, submitted] + types: [synchronize, opened, ready_for_review, review_requested] jobs: auto-merge: @@ -25,60 +25,47 @@ jobs: set -eu echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" - echo "base: ${BASE_REF}" echo # 只合develop分支 if [ "$BASE_REF" != "develop" ]; then - echo "ℹ️ 目标分支不是develop,跳过自动合并" + echo "Skip: 目标分支不是develop" exit 0 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) + # 判断是否纯前端改动 + 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 - 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)") + echo "纯前端改动,只检查Frontend Lint" else CONTEXTS=( "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" "CI/CD Pipeline / Unit Tests (pull_request)" "CI/CD Pipeline / Frontend Lint (pull_request)" ) + echo "检查全部三门禁" fi - - echo "需要通过的CI检查: ${#CONTEXTS[@]} 项" - for ctx in "${CONTEXTS[@]}"; do - echo " - $ctx" - done echo - # 轮询等待CI + 审批,最多30分钟(180次x10秒) + # 轮询等待,最多30分钟(180次x10秒) for attempt in $(seq 1 180); do ALL_SUCCESS=true ANY_FAILED=false echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" - # 检查每个context状态 + # 检查CI状态 for ctx in "${CONTEXTS[@]}"; do STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$ctx") - echo " CI: $ctx: $STATE" - + echo " CI: ${ctx##*/}: $STATE" if [ "$STATE" != "success" ]; then ALL_SUCCESS=false fi @@ -88,36 +75,25 @@ jobs: done # 检查审批状态 - REVIEW_COUNT=$(curl -s -H "Authorization: token ${MERGE_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \ - | python3 -c " -import sys, json -reviews = json.load(sys.stdin) -# 统计APPROVED的人数(去重) -approvers = set() -for r in reviews: - if r.get('state') == 'APPROVED': - approvers.add(r.get('user', {}).get('login', '')) -print(len(approvers)) -") - echo " 审批: ${REVIEW_COUNT} 个 APPROVED" + APPROVAL_RESULT=$(python3 scripts/check_pr_approval.py "$MERGE_TOKEN" "$GITHUB_REPOSITORY" "$PR_NUMBER" 1) + echo " 审批: $APPROVAL_RESULT" HAS_APPROVAL=false - if [ "$REVIEW_COUNT" -ge 1 ]; then + if echo "$APPROVAL_RESULT" | grep -q '^approved'; then HAS_APPROVAL=true fi # 全部满足 → 合并 if [ "$ALL_SUCCESS" = "true" ] && [ "$HAS_APPROVAL" = "true" ]; then echo - echo "✅ CI全绿 + 审批通过,自动合并 PR #${PR_NUMBER}" + echo "CI全绿 + 审批通过,执行自动合并" - # 幂等检查:确认PR状态还是open + # 幂等检查: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_NUMBER} 状态为 ${PR_STATE},无需合并" + echo "PR状态为 ${PR_STATE},无需合并" exit 0 fi @@ -132,27 +108,23 @@ print(len(approvers)) echo "合并API HTTP状态: $HTTP_CODE" if [ "$HTTP_CODE" = "200" ]; then - echo "✅ 自动合并成功" + echo "自动合并成功" exit 0 elif [ "$HTTP_CODE" = "405" ]; then - echo "⚠️ 合并失败(405 Method Not Allowed),可能是有冲突或门禁未通过" - # 留评论提醒 + echo "合并失败(405),可能有冲突或门禁未通过" curl -s -X POST \ -H "Authorization: token ${MERGE_TOKEN}" \ -H "Content-Type: application/json" \ - -d "{\"body\": \"🤖 自动合并失败:PR可能有冲突或门禁未全部通过,请人工检查。\"}" \ + -d '{"body": "Auto merge failed: PR may have conflicts or unresolved checks. Please review manually."}' \ "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" > /dev/null 2>&1 || true - echo "ℹ️ 已在PR留言提醒" exit 0 else - echo "❌ 自动合并失败 (HTTP $HTTP_CODE)" + echo "自动合并失败 (HTTP $HTTP_CODE)" cat /tmp/merge_resp.json 2>/dev/null || true - echo - # 留评论提醒 curl -s -X POST \ -H "Authorization: token ${MERGE_TOKEN}" \ -H "Content-Type: application/json" \ - -d "{\"body\": \"🤖 自动合并失败(HTTP ${HTTP_CODE}),请人工检查。\"}" \ + -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 @@ -160,7 +132,7 @@ print(len(approvers)) if [ "$ANY_FAILED" = "true" ]; then echo - echo "❌ CI检查有失败项,不自动合并" + echo "CI有失败项,不自动合并" exit 0 fi @@ -168,5 +140,5 @@ print(len(approvers)) done echo - echo "⏰ 等待超时(30分钟),CI或审批尚未满足条件" + echo "等待超时(30分钟)" exit 0 -- 2.54.0 From 7bc7fb55f619b967dbace919cb2bbae6163693ab Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 11:31:50 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(ci):=20=E6=96=B0=E5=A2=9EPR=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E6=A3=80=E6=9F=A5=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用于auto-merge workflow检查PR是否有足够APPROVED审批 --- scripts/check_pr_approval.py | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 scripts/check_pr_approval.py diff --git a/scripts/check_pr_approval.py b/scripts/check_pr_approval.py new file mode 100644 index 000000000..5f08a4c72 --- /dev/null +++ b/scripts/check_pr_approval.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""检查PR是否有至少N个APPROVED审批。 + +用法: python3 check_pr_approval.py +返回: 打印 "approved" 或 "pending" +""" + +import json +import sys +import urllib.request + + +def main(): + if len(sys.argv) != 5: + print("pending") + return + + token = sys.argv[1] + repo = sys.argv[2] + pr_number = sys.argv[3] + min_approval = int(sys.argv[4]) + + api_url = f"https://git.xiaoxiajianji.com/api/v1/repos/{repo}/pulls/{pr_number}/reviews" + req = urllib.request.Request(api_url, headers={"Authorization": f"token {token}"}) + + try: + with urllib.request.urlopen(req, timeout=30) as resp: + reviews = json.loads(resp.read().decode()) + except Exception: + print("pending") + return + + # 统计APPROVED的人数(去重,同一人多次审批只算一次) + approvers = set() + for r in reviews: + if r.get("state") == "APPROVED": + approvers.add(r.get("user", {}).get("login", "")) + + if len(approvers) >= min_approval: + print(f"approved ({len(approvers)})") + else: + print(f"pending ({len(approvers)})") + + +if __name__ == "__main__": + main() -- 2.54.0 From 7f8c648bb7aa68a64ccb37273e5cfa8f39feec29 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 11:34:01 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8Dauto-merge=20b?= =?UTF-8?q?ash=E8=84=9A=E6=9C=AC=E5=BC=95=E5=8F=B7=E4=B8=8D=E9=97=AD?= =?UTF-8?q?=E5=90=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml index 1b95641fd..ab4738113 100644 --- a/.gitea/workflows/auto-merge.yml +++ b/.gitea/workflows/auto-merge.yml @@ -36,7 +36,7 @@ jobs: # 判断是否纯前端改动 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)]) + | 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)) -- 2.54.0 From e388f6cb88684cbb2eb70415e6657e0f4b46df41 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 11:46:14 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(ci):=20auto-merge=E5=A2=9E=E5=8A=A0Inte?= =?UTF-8?q?gration=20Tests=E9=97=A8=E7=A6=81=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/auto-merge.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml index ab4738113..bc0db30ee 100644 --- a/.gitea/workflows/auto-merge.yml +++ b/.gitea/workflows/auto-merge.yml @@ -50,8 +50,9 @@ jobs: "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" "CI/CD Pipeline / Unit Tests (pull_request)" "CI/CD Pipeline / Frontend Lint (pull_request)" + "CI/CD Pipeline / Integration Tests (pull_request)" ) - echo "检查全部三门禁" + echo "检查全部四门禁" fi echo -- 2.54.0