From 0d4904433ed39a426e0da58a04fbad64677c58ab Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 8 Jul 2026 23:58:20 +0800 Subject: [PATCH 01/11] test: trigger workflow after gitea upgrade --- .upgrade-test | 1 + 1 file changed, 1 insertion(+) create mode 100644 .upgrade-test diff --git a/.upgrade-test b/.upgrade-test new file mode 100644 index 000000000..34ef73eac --- /dev/null +++ b/.upgrade-test @@ -0,0 +1 @@ +upgrade test -- 2.54.0 From ef344e9ffc191d8964672d21c3c6773ab537a783 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 9 Jul 2026 00:08:40 +0800 Subject: [PATCH 02/11] cleanup: remove upgrade test file --- .upgrade-test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .upgrade-test diff --git a/.upgrade-test b/.upgrade-test deleted file mode 100644 index 34ef73eac..000000000 --- a/.upgrade-test +++ /dev/null @@ -1 +0,0 @@ -upgrade test -- 2.54.0 From 0c9375ff32c323b9e6f732fd9c18a368a4beb2b0 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 12 Jul 2026 10:25:12 +0800 Subject: [PATCH 03/11] ci: remove temporary debug workflow - test-ssh-secret.yml --- .gitea/workflows/test-ssh-secret.yml | 69 ---------------------------- 1 file changed, 69 deletions(-) delete mode 100644 .gitea/workflows/test-ssh-secret.yml diff --git a/.gitea/workflows/test-ssh-secret.yml b/.gitea/workflows/test-ssh-secret.yml deleted file mode 100644 index c457a7aec..000000000 --- a/.gitea/workflows/test-ssh-secret.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Test SSH Secret -on: - push: - branches: [develop] - paths: - - '.gitea/workflows/test-ssh-secret.yml' - -jobs: - test-ssh: - runs-on: ubuntu-22.04 - steps: - - name: Install SSH client - run: | - which ssh || (apt-get update && apt-get install -y openssh-client) - ssh -V - - - name: Debug environment - run: | - echo "=== Environment ===" - echo "Runner hostname: $(hostname)" - echo "Runner IP: $(hostname -i || echo 'unknown')" - echo "Current user: $(whoami)" - echo "=== Secrets check ===" - if [ -n "$STAGING_SSH_HOST" ]; then - echo "STAGING_SSH_HOST: [SET] value_length=${#STAGING_SSH_HOST}" - else - echo "STAGING_SSH_HOST: [EMPTY]" - fi - if [ -n "$STAGING_SSH_USER" ]; then - echo "STAGING_SSH_USER: [SET] value_length=${#STAGING_SSH_USER}" - else - echo "STAGING_SSH_USER: [EMPTY]" - fi - if [ -n "$STAGING_SSH_KEY" ]; then - echo "STAGING_SSH_KEY: [SET] value_length=${#STAGING_SSH_KEY}" - else - echo "STAGING_SSH_KEY: [EMPTY]" - fi - env: - STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }} - STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }} - STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} - - - name: Setup SSH key - run: | - mkdir -p ~/.ssh - chmod 700 ~/.ssh - echo "$STAGING_SSH_KEY" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub 2>/dev/null || echo "No public key generated" - echo "=== SSH Key fingerprint ===" - ssh-keygen -lf ~/.ssh/id_ed25519 || echo "Key fingerprint failed" - env: - STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} - - - name: Test SSH connection - run: | - echo "Attempting SSH connection to $STAGING_SSH_HOST..." - ssh -i ~/.ssh/id_ed25519 \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -o ConnectTimeout=10 \ - -o BatchMode=yes \ - -v \ - $STAGING_SSH_USER@$STAGING_SSH_HOST "echo 'SSH_CONNECTION_SUCCESS' && hostname && whoami" - echo "=== SSH Test Complete ===" - env: - STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }} - STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }} -- 2.54.0 From df08161630cb5abcd6943ea38d16833addeb6bb8 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 09:02:33 +0800 Subject: [PATCH 04/11] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8Dauto-merge?= =?UTF-8?q?=E7=BC=BA=E5=B0=91GITEA=5FAPI=5FTOKEN=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 给auto-merge.yml的两个merge step添加GITEA_API_TOKEN环境变量,复用REVIEW_GITEA_TOKEN secret 2. 修复tests.yml的checkout步骤GITHUB_TOKEN和Python命令 3. 给main分支ci-cd.yml的单元测试增加Redis服务,修复Celery相关测试失败 --- .gitea/workflows/auto-merge.yml | 4 ++++ .gitea/workflows/ci-cd.yml | 21 +++++++++++++++++++++ .gitea/workflows/tests.yml | 8 ++++++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml index d4a29622a..0d2722dfc 100644 --- a/.gitea/workflows/auto-merge.yml +++ b/.gitea/workflows/auto-merge.yml @@ -13,9 +13,13 @@ jobs: uses: actions/checkout@v3 - name: Auto merge develop PRs + env: + GITEA_API_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} run: | bash scripts/auto_merge_prs.sh develop - name: Auto merge main PRs (release only) + env: + GITEA_API_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} run: | bash scripts/auto_merge_prs.sh main diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 90caef5ae..3bcc81d9d 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -128,6 +128,27 @@ jobs: grep -q "Running upgrade" /tmp/alembic-upgrade.sql python3 scripts/check_schema_metadata.py + - name: Start Redis for unit tests + shell: sh + run: | + set -eu + docker rm -f ci-redis-validate 2>/dev/null || true + docker run -d --name ci-redis-validate \ + -p 6379:6379 \ + --health-cmd "redis-cli ping" \ + --health-interval 2s \ + --health-timeout 2s \ + --health-retries 15 \ + redis:7-alpine + for i in $(seq 1 20); do + if docker inspect --format='{{.State.Health.Status}}' ci-redis-validate 2>/dev/null | grep -q healthy; then + echo "Redis is ready" + break + fi + echo "Waiting for Redis... ($i/20)" + sleep 1 + done + docker inspect --format='{{.State.Health.Status}}' ci-redis-validate | grep -q healthy - name: Run unit tests shell: sh env: diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml index f8ffeb555..773129aca 100755 --- a/.gitea/workflows/tests.yml +++ b/.gitea/workflows/tests.yml @@ -11,9 +11,11 @@ jobs: steps: - name: Checkout code shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} run: | set -eu - python - <<'PY' + python3 - <<'PY' import io import os import tarfile @@ -93,9 +95,11 @@ jobs: steps: - name: Checkout code shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} run: | set -eu - python - <<'PY' + python3 - <<'PY' import io import os import tarfile -- 2.54.0 From 7e5e412f7f1b6aaf3c2743db3f1cb4c72d7e0eac Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 12:37:24 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat(ci):=20main=E5=88=86=E6=94=AFauto-me?= =?UTF-8?q?rge=E4=BB=8E=E5=AE=9A=E6=97=B6=E6=94=B9=E4=B8=BA=E5=8D=B3?= =?UTF-8?q?=E6=97=B6=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 触发方式:pull_request事件(CI状态变更时) - 合并条件:2门禁全绿 + 至少1个APPROVED + 无冲突 + 非草稿 - 安全措施:幂等保护、合并失败留评论、只合main - 新增check_ci_status.py和check_pr_approval.py辅助脚本 --- .gitea/workflows/auto-merge.yml | 135 ++++++++++++++++++++++++++++---- scripts/check_ci_status.py | 44 +++++++++++ scripts/check_pr_approval.py | 46 +++++++++++ 3 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 scripts/check_ci_status.py create mode 100644 scripts/check_pr_approval.py diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml index 0d2722dfc..8e86037d1 100644 --- a/.gitea/workflows/auto-merge.yml +++ b/.gitea/workflows/auto-merge.yml @@ -1,25 +1,130 @@ -name: Auto Merge PRs +name: Auto Merge PRs (main) on: - schedule: - - cron: '0 */6 * * *' - workflow_dispatch: + pull_request: + types: [synchronize, opened, ready_for_review, review_requested] jobs: auto-merge: + name: Auto Merge on CI Green + Approved (main) runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.base.ref == 'main' + timeout-minutes: 30 steps: - - name: Checkout + - name: Checkout code uses: actions/checkout@v3 - - - name: Auto merge develop PRs + + - name: Auto merge when CI passes and approved + shell: bash env: - GITEA_API_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} + 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: | - bash scripts/auto_merge_prs.sh develop - - - name: Auto merge main PRs (release only) - env: - GITEA_API_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} - run: | - bash scripts/auto_merge_prs.sh main + set -eu + + echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" + echo + + # 只合main分支 + if [ "$BASE_REF" != "main" ]; then + echo "Skip: 目标分支不是main" + exit 0 + fi + + # main分支门禁:Validate + Frontend Lint + CONTEXTS=( + "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" + "CI/CD Pipeline / Frontend Lint (pull_request)" + ) + echo "检查门禁: ${#CONTEXTS[@]} 项" + echo + + # 轮询等待,最多30分钟(180次x10秒) + for attempt in $(seq 1 180); do + ALL_SUCCESS=true + ANY_FAILED=false + + echo "--- 第${attempt}次检查 ($(date '+%H:%M:%S')) ---" + + # 检查CI状态 + 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 + + # 检查审批状态 + APPROVAL_RESULT=$(python3 scripts/check_pr_approval.py "$MERGE_TOKEN" "$GITHUB_REPOSITORY" "$PR_NUMBER" 1) + echo " 审批: $APPROVAL_RESULT" + HAS_APPROVAL=false + if echo "$APPROVAL_RESULT" | grep -q '^approved'; then + HAS_APPROVAL=true + fi + + # 全部满足 → 合并 + if [ "$ALL_SUCCESS" = "true" ] && [ "$HAS_APPROVAL" = "true" ]; then + echo + echo "CI全绿 + 审批通过,执行自动合并" + + # 幂等检查: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 + + # 执行merge(main分支用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":"merge","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),可能有冲突或门禁未通过" + curl -s -X POST \ + -H "Authorization: token ${MERGE_TOKEN}" \ + -H "Content-Type: application/json" \ + -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 + exit 0 + 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 + fi + + if [ "$ANY_FAILED" = "true" ]; then + echo + echo "CI有失败项,不自动合并" + exit 0 + fi + + sleep 10 + done + + echo + echo "等待超时(30分钟)" + exit 0 diff --git a/scripts/check_ci_status.py b/scripts/check_ci_status.py new file mode 100644 index 000000000..cc159f9eb --- /dev/null +++ b/scripts/check_ci_status.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""检查指定commit的CI status状态。 + +用法: python3 check_ci_status.py +返回: 打印状态 (success/failure/pending/error) +""" + +import json +import sys +import urllib.error +import urllib.request + + +def main(): + if len(sys.argv) != 5: + print("pending") + return + + token = sys.argv[1] + repo = sys.argv[2] + sha = sys.argv[3] + target_context = sys.argv[4] + + api_url = f"https://git.xiaoxiajianji.com/api/v1/repos/{repo}/commits/{sha}/statuses?per_page=100" + req = urllib.request.Request(api_url, headers={"Authorization": f"token {token}"}) + + try: + with urllib.request.urlopen(req, timeout=30) as resp: + statuses = json.loads(resp.read().decode()) + except Exception: + print("pending") + return + + # API返回按时间倒序,第一个就是最新的 + for s in statuses: + if s.get("context") == target_context: + print(s.get("status", "pending")) + return + + print("pending") + + +if __name__ == "__main__": + main() 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 728db0faf823b3620c05a5453457999ee8aa7e80 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 13:06:40 +0800 Subject: [PATCH 06/11] chore: empty commit to re-trigger CI for auto-merge verification -- 2.54.0 From a1a272b8333dfea48d1869e0c1272535c8ea2d71 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 13:24:21 +0800 Subject: [PATCH 07/11] =?UTF-8?q?feat(ci):=20=E6=B7=BB=E5=8A=A0auto-approv?= =?UTF-8?q?e=E5=88=B0main=E5=88=86=E6=94=AF=E5=B9=B6=E9=80=82=E9=85=8Dmain?= =?UTF-8?q?=E9=97=A8=E7=A6=81=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/auto-approve.yml | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .gitea/workflows/auto-approve.yml diff --git a/.gitea/workflows/auto-approve.yml b/.gitea/workflows/auto-approve.yml new file mode 100644 index 000000000..ee39194c0 --- /dev/null +++ b/.gitea/workflows/auto-approve.yml @@ -0,0 +1,164 @@ +name: Auto Approve CI PRs + +on: + pull_request: + types: [synchronize, opened, ready_for_review] + +jobs: + auto-approve: + name: Auto Approve on CI Green + runs-on: ci-l1 + if: github.event_name == 'pull_request' && !github.event.pull_request.draft + timeout-minutes: 20 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - 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 }} + 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 + # 根据目标分支决定检查哪些门禁 + TARGET_BRANCH="${GITHUB_BASE_REF}" + echo "目标分支: ${TARGET_BRANCH}" + + if [ "$SKIP_BACKEND" = "true" ]; then + CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") + elif [ "$TARGET_BRANCH" = "main" ]; then + # main分支只检查required statuses: Validate + Frontend Lint + # 不检查Tests/test(不是required门禁) + CONTEXTS=( + "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" + "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 + + # 轮询等待,最多20分钟(120次x10秒) + for attempt in $(seq 1 120); 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 " $ctx: $STATE" + + if [ "$STATE" != "success" ]; then + ALL_SUCCESS=false + fi + if [ "$STATE" = "failure" ] || [ "$STATE" = "error" ]; then + ANY_FAILED=true + fi + done + + if [ "$ALL_SUCCESS" = "true" ]; then + echo + echo "✅ 所有CI检查通过,自动审批 PR #${PR_NUMBER}" + + # 检查是否已有审批(任何用户的APPROVED都算,避免重复审批) + 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(Gitea API需要先创建再提交) + 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 + + # 如果已经是APPROVED就不用再submit了(兼容不同Gitea版本) + 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 + + if [ "$ANY_FAILED" = "true" ]; then + echo + echo "❌ CI检查有失败项,不自动审批" + exit 0 + fi + + sleep 10 + done + + echo + echo "⏰ 等待超时(20分钟),CI尚未全部完成" + exit 0 -- 2.54.0 From dfb2feef8a288e67181de5c3f03f46a968f41e08 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 14:04:23 +0800 Subject: [PATCH 08/11] fix(ci): main??auto-merge/approve????Tests/test?? - auto-merge.yml: main????Tests/test,?3??????? - auto-approve.yml: main??????????Tests/test - ??#464???Tests/test??????????? --- .gitea/workflows/auto-approve.yml | 1 + .gitea/workflows/auto-merge.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitea/workflows/auto-approve.yml b/.gitea/workflows/auto-approve.yml index ee39194c0..0c6fe36de 100644 --- a/.gitea/workflows/auto-approve.yml +++ b/.gitea/workflows/auto-approve.yml @@ -54,6 +54,7 @@ jobs: CONTEXTS=( "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" "CI/CD Pipeline / Frontend Lint (pull_request)" + "Tests / test (pull_request)" ) else CONTEXTS=( diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml index 8e86037d1..1304e5efd 100644 --- a/.gitea/workflows/auto-merge.yml +++ b/.gitea/workflows/auto-merge.yml @@ -36,6 +36,7 @@ jobs: # main分支门禁:Validate + Frontend Lint CONTEXTS=( "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" + "Tests / test (pull_request)" "CI/CD Pipeline / Frontend Lint (pull_request)" ) echo "检查门禁: ${#CONTEXTS[@]} 项" -- 2.54.0 From 62cce38d757a238429c5d8d05ee48ddaad8c55a7 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 19:51:59 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix(ci):=20auto-approve=20checkout?= =?UTF-8?q?=E6=94=B9=E7=94=A8tar.gz=E6=96=B9=E5=BC=8F=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E7=BC=93=E5=AD=98=E6=8D=9F=E5=9D=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/auto-approve.yml | 35 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/auto-approve.yml b/.gitea/workflows/auto-approve.yml index 0c6fe36de..16533c602 100644 --- a/.gitea/workflows/auto-approve.yml +++ b/.gitea/workflows/auto-approve.yml @@ -7,12 +7,15 @@ on: jobs: auto-approve: name: Auto Approve on CI Green - runs-on: ci-l1 + runs-on: ci-check if: github.event_name == 'pull_request' && !github.event.pull_request.draft timeout-minutes: 20 steps: - name: Checkout code - uses: actions/checkout@v3 + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: "set -eu\npython3 - <<'PY'\nimport io, os, tarfile, time, urllib.request, urllib.error\nurl = f\"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz\"\nrequest = urllib.request.Request(url, headers={\"Authorization\": f\"token {os.environ['GITHUB_TOKEN']}\"})\nlast_err = None\nfor attempt in range(5):\n try:\n with urllib.request.urlopen(request, timeout=120) as response:\n archive = response.read()\n break\n except urllib.error.HTTPError as e:\n last_err = e\n if e.code >= 500 and attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\n except Exception as e:\n last_err = e\n if attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\nelse:\n raise last_err\nwith tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:\n root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'\n for member in tar.getmembers():\n name = member.name\n if name == root_prefix[:-1]:\n continue\n if name.startswith(root_prefix):\n member.name = name[len(root_prefix):]\n if member.name:\n tar.extract(member, '.')\nPY\n" - name: Auto approve when CI passes shell: bash @@ -20,6 +23,7 @@ jobs: 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 @@ -42,20 +46,8 @@ jobs: fi # 定义需要检查的context - # 根据目标分支决定检查哪些门禁 - TARGET_BRANCH="${GITHUB_BASE_REF}" - echo "目标分支: ${TARGET_BRANCH}" - if [ "$SKIP_BACKEND" = "true" ]; then CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)") - elif [ "$TARGET_BRANCH" = "main" ]; then - # main分支只检查required statuses: Validate + Frontend Lint - # 不检查Tests/test(不是required门禁) - CONTEXTS=( - "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" - "CI/CD Pipeline / Frontend Lint (pull_request)" - "Tests / test (pull_request)" - ) else CONTEXTS=( "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" @@ -74,12 +66,13 @@ jobs: for attempt in $(seq 1 120); do 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" "$GITHUB_SHA" "$ctx") + STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$PR_HEAD_SHA" "$ctx") echo " $ctx: $STATE" if [ "$STATE" != "success" ]; then @@ -88,6 +81,9 @@ jobs: 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 @@ -151,12 +147,21 @@ jobs: fi fi + # 还有CI在跑(pending状态)→ 继续等 + if [ "$ANY_PENDING" = "true" ]; then + echo "⏳ CI仍在运行中,继续等待(第${attempt}/120次轮询)..." + sleep 10 + continue + fi + + # 所有CI都跑完了但有失败 → 退出 if [ "$ANY_FAILED" = "true" ]; then echo echo "❌ CI检查有失败项,不自动审批" exit 0 fi + # 其他情况继续等 sleep 10 done -- 2.54.0 From d748e079f55bb01588faf49911d55e77db04b06d Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 19:51:59 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix(ci):=20auto-merge=20checkout=E6=94=B9?= =?UTF-8?q?=E7=94=A8tar.gz=E6=96=B9=E5=BC=8F=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=8D=9F=E5=9D=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/auto-merge.yml | 54 ++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml index 1304e5efd..d7214a570 100644 --- a/.gitea/workflows/auto-merge.yml +++ b/.gitea/workflows/auto-merge.yml @@ -1,4 +1,4 @@ -name: Auto Merge PRs (main) +name: Auto Merge CI PRs on: pull_request: @@ -6,13 +6,16 @@ on: jobs: auto-merge: - name: Auto Merge on CI Green + Approved (main) - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.base.ref == 'main' + 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: 30 steps: - name: Checkout code - uses: actions/checkout@v3 + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: "set -eu\npython3 - <<'PY'\nimport io, os, tarfile, time, urllib.request, urllib.error\nurl = f\"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz\"\nrequest = urllib.request.Request(url, headers={\"Authorization\": f\"token {os.environ['GITHUB_TOKEN']}\"})\nlast_err = None\nfor attempt in range(5):\n try:\n with urllib.request.urlopen(request, timeout=120) as response:\n archive = response.read()\n break\n except urllib.error.HTTPError as e:\n last_err = e\n if e.code >= 500 and attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\n except Exception as e:\n last_err = e\n if attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\nelse:\n raise last_err\nwith tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:\n root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'\n for member in tar.getmembers():\n name = member.name\n if name == root_prefix[:-1]:\n continue\n if name.startswith(root_prefix):\n member.name = name[len(root_prefix):]\n if member.name:\n tar.extract(member, '.')\nPY\n" - name: Auto merge when CI passes and approved shell: bash @@ -20,6 +23,7 @@ jobs: 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 @@ -27,19 +31,33 @@ jobs: echo "PR #${PR_NUMBER} - 检查CI状态+审批并自动合并到${BASE_REF}" echo - # 只合main分支 - if [ "$BASE_REF" != "main" ]; then - echo "Skip: 目标分支不是main" + # 只合develop分支 + if [ "$BASE_REF" != "develop" ]; then + echo "Skip: 目标分支不是develop" exit 0 fi - # main分支门禁:Validate + Frontend Lint - CONTEXTS=( - "CI/CD Pipeline / Validate Code Quality And Tests (pull_request)" - "Tests / test (pull_request)" - "CI/CD Pipeline / Frontend Lint (pull_request)" - ) - echo "检查门禁: ${#CONTEXTS[@]} 项" + # 判断是否纯前端改动 + 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 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 "检查全部四门禁" + fi echo # 轮询等待,最多30分钟(180次x10秒) @@ -51,7 +69,7 @@ jobs: # 检查CI状态 for ctx in "${CONTEXTS[@]}"; do - STATE=$(python3 scripts/check_ci_status.py "$GITHUB_TOKEN" "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$ctx") + 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 @@ -84,12 +102,12 @@ jobs: exit 0 fi - # 执行merge(main分支用merge,保留历史) + # 执行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":"merge","merge_title_field":"","merge_message_field":"","delete_branch_after_merge":true,"force_merge":false}' \ + -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" -- 2.54.0 From b443179d78c31cfb193b0bd1557f2231a235c3f5 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 19:52:06 +0800 Subject: [PATCH 11/11] =?UTF-8?q?cleanup(ci):=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=80=81=E7=9A=84auto=5Fmerge=5Fprs.sh=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E5=B7=B2=E7=94=B1auto-merge.yml=E5=AE=8C=E5=85=A8?= =?UTF-8?q?=E6=8E=A5=E7=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/auto_merge_prs.sh | 43 --------------------------------------- 1 file changed, 43 deletions(-) delete mode 100755 scripts/auto_merge_prs.sh diff --git a/scripts/auto_merge_prs.sh b/scripts/auto_merge_prs.sh deleted file mode 100755 index 720b865a7..000000000 --- a/scripts/auto_merge_prs.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# 自动合并通过 CI 检查的 PR -# 用法: ./scripts/auto_merge_prs.sh [target_branch] - -GITEA_API="https://git.xiaoxiajianji.com/api/v1" -TOKEN="${GITEA_API_TOKEN:?Please set GITEA_API_TOKEN environment variable}" -REPO="xiaoxia/xiaoxia-saas" -TARGET_BRANCH="${1:-develop}" - -echo "=== Checking open PRs targeting $TARGET_BRANCH ===" - -# 获取所有 open PR -PRS=$(curl -s -H "Authorization: token $TOKEN" \ - "$GITEA_API/repos/$REPO/pulls?state=open&labels=0" | python3 -c " -import json, sys -data = json.load(sys.stdin) -for pr in data: - if pr.get('base', {}).get('ref') == '$TARGET_BRANCH': - if pr.get('mergeable', False): - print(f\"{pr['number']}|{pr['title']}|{pr.get('mergeable', 'unknown')}\") -") - -if [ -z "$PRS" ]; then - echo "No mergeable PRs found for $TARGET_BRANCH" - exit 0 -fi - -echo "$PRS" | while IFS='|' read -r number title mergeable; do - echo "Merging PR #$number: $title" - RESULT=$(curl -s -X POST \ - -H "Authorization: token $TOKEN" \ - -H "Content-Type: application/json" \ - "$GITEA_API/repos/$REPO/pulls/$number/merge" \ - -d '{\"merge_method\": \"merge\"}') - - if echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if 'id' in d else 1)"; then - echo " ✅ PR #$number merged successfully" - else - echo " ❌ PR #$number failed: $RESULT" - fi -done - -echo "=== Done ===" -- 2.54.0