diff --git a/.gitea/workflows/ci-pipeline.yml b/.gitea/workflows/ci-pipeline.yml index fb51baa41..dca9c415b 100755 --- a/.gitea/workflows/ci-pipeline.yml +++ b/.gitea/workflows/ci-pipeline.yml @@ -58,10 +58,11 @@ jobs: echo "::warning::PR #${PR_NUMBER} 已合并,测试由合并后 push 流水线承接,PR 侧测试类 job 跳过" exit 0 fi - # 情形2:同一 head_sha 已有在跑/排队的 push 流水线(rebase/ff 合并竞态) + # 情形2:同一 head_sha 已有在跑/排队/已成功的 push 流水线(rebase/ff 合并竞态; + # 已成功也去重——竞态窗口内两边都通过判断时,PR侧再跑全量测试纯属重复) DUP=$(curl -sfH "Authorization: token $GITHUB_TOKEN" \ "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${HEAD_SHA}&per_page=30" \ - | python3 -c "import json,sys; d=json.load(sys.stdin); runs=d if isinstance(d,list) else d.get('workflow_runs',d.get('runs',[])); hit=[r for r in runs if r.get('event')=='push' and r.get('status') in ('in_progress','queued','waiting','pending')]; print('true' if hit else 'false')" || echo false) + | python3 -c "import json,sys; d=json.load(sys.stdin); runs=d if isinstance(d,list) else d.get('workflow_runs',d.get('runs',[])); hit=[r for r in runs if r.get('event')=='push' and (r.get('status') in ('in_progress','queued','waiting','pending') or r.get('conclusion')=='success')]; print('true' if hit else 'false')" || echo false) if [ "$DUP" = "true" ]; then echo "skip_tests=true" >> $GITHUB_OUTPUT echo "reason=duplicate-push-run-active" >> $GITHUB_OUTPUT @@ -144,6 +145,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -244,6 +251,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -325,6 +338,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -408,6 +427,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -488,6 +513,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -542,6 +573,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -604,6 +641,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -695,6 +738,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -873,6 +922,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -1246,6 +1301,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -1293,6 +1354,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh @@ -1367,6 +1434,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | curl -sfH "Authorization: token $GITHUB_TOKEN" -o /tmp/_ci_checkout.sh "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" && bash /tmp/_ci_checkout.sh + - name: Zombie run selfcheck + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: bash scripts/ci/ci_run_selfcheck.sh - name: Record job start time shell: sh run: bash scripts/ci/step_timer_start.sh diff --git a/scripts/ci/ci_run_selfcheck.sh b/scripts/ci/ci_run_selfcheck.sh new file mode 100755 index 000000000..7b57de920 --- /dev/null +++ b/scripts/ci/ci_run_selfcheck.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# CI job 运行前自检 —— 防 Gitea 1.26 调度 bug: +# 被 concurrency cancel 的 run,其已派发的 job 可能在数小时后被调度器复活重跑 +# (2026-08-30 实测:18:46 取消的 run,21:46 复活 attempt=2,与正常流水线抢 runner 1.5 小时)。 +# job 启动第一步先实时查本 run 状态,若已终结(cancelled/skipped),容器立即退出不烧资源。 +# +# 用法:在重量级 job(Validate/Unit Tests/Integration/Frontend/Build/E2E 类)的 +# 首个 step 中加入:bash scripts/ci/ci_run_selfcheck.sh +set -u + +echo "▶ CI 运行前自检(僵尸 run 复活防护)..." + +if [ -z "${GITHUB_API_URL:-}" ] || [ -z "${GITHUB_REPOSITORY:-}" ] || [ -z "${GITHUB_TOKEN:-}" ] || [ -z "${GITHUB_RUN_ID:-}" ]; then + echo "⚠️ 缺少 GITHUB_* 环境变量(GITHUB_RUN_ID/GITHUB_TOKEN),跳过自检(不阻断)" + exit 0 +fi + +CONCLUSION=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN}" --max-time 10 \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 2>/dev/null \ + | python3 -c "import json,sys; print(json.load(sys.stdin).get('conclusion') or 'running')" 2>/dev/null || echo "unknown") + +case "${CONCLUSION}" in + cancelled|skipped) + echo "::error::本 run(${GITHUB_RUN_ID}) 状态已为 ${CONCLUSION},却仍被调度器派发执行——判定为 Gitea 复活的僵尸 job,立即退出,不再占用 runner" + exit 78 # EX_CONFIG:非零退出让调度器知晓该 job 终结(避免被当成功空跑) + ;; + running|unknown|"") + # unknown 时不阻断(API 临时故障),正常执行 + echo "✅ run ${GITHUB_RUN_ID} 状态正常(${CONCLUSION:-running}),继续执行" + ;; + *) + echo "✅ run ${GITHUB_RUN_ID} 结论=${CONCLUSION}(已终结但非取消),继续执行" + ;; +esac