fix(pr-ci-wait): CI-history primary tier — close webhook-lag false-green (#550)
F-06 follow-up per Mos ruling. The no-CI fast-exit was a pure empty-poll streak (NO_CI_MAX×interval ≈ 45s), so a slow-to-register pipeline (webhook/queue lag) looked like 'no CI' and could false-green a merge gate before the pipeline existed. Two-tier no-CI determination: - PRIMARY: probe the repo's DEFAULT BRANCH commit status once at startup. If it has CI history, the repo runs CI → an empty status on the PR head means the pipeline has not REGISTERED yet → never fast-green; poll until it registers or timeout (both safe). Closes the webhook-lag false-green. - SECONDARY: the empty-poll streak fast-exit now applies ONLY to genuinely CI-less repos (default branch also has no CI history). Preserves the original no-CI win. - Probe failure → conservative REPO_HAS_CI=1 (assume CI; wait-then-timeout beats false-green). All early returns are explicit 'return 0' + guarded call so the probe can never abort under set -e. Verified: bash -n + shellcheck clean; behavioral harness covers established-repo (stays 1), CI-less (→0), empty-branch/probe-fail (conservative 1), and the no-status gate (has-CI never fast-greens, CI-less fast-exits). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kt2D8TsnDwhtzEAPijsNmR
This commit is contained in:
@@ -147,6 +147,21 @@ gitea_get_commit_status_json() {
|
|||||||
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
|
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitea_get_default_branch() {
|
||||||
|
local host="$1"
|
||||||
|
local repo="$2"
|
||||||
|
local token="$3"
|
||||||
|
local url="https://${host}/api/v1/repos/${repo}"
|
||||||
|
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
print((json.load(sys.stdin) or {}).get("default_branch", ""))
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
github_get_default_branch() {
|
||||||
|
gh api "repos/${OWNER}/${REPO}" --jq '.default_branch'
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-n|--number)
|
-n|--number)
|
||||||
@@ -250,10 +265,48 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Count consecutive polls that find NO pipeline/status at all. A repo/commit with
|
# No-CI determination is TWO-TIER (primary: CI history; secondary: empty-poll streak).
|
||||||
# no CI configured (e.g. device-imaging class) would otherwise burn the full
|
#
|
||||||
# timeout in the pending/unknown branch. After NO_CI_MAX such polls, fast-exit 0
|
# PRIMARY — "does this repo run CI at all?" Probed once, up front, from the DEFAULT
|
||||||
# with a clear "no CI configured" message — distinct from a real failure.
|
# BRANCH's commit status. A repo whose default branch carries CI statuses
|
||||||
|
# demonstrably runs CI, so an EMPTY status on the PR head means the pipeline simply
|
||||||
|
# has not registered YET (webhook/queue lag) — NOT that the repo is CI-less. In that
|
||||||
|
# case we must NEVER fast-green; we keep polling until the pipeline registers or the
|
||||||
|
# timeout fires (both safe). This closes the webhook-lag false-green: a slow-to-
|
||||||
|
# register pipeline feeding a merge gate can no longer be mistaken for "no CI".
|
||||||
|
#
|
||||||
|
# SECONDARY — the empty-poll streak below applies ONLY to genuinely CI-less repos
|
||||||
|
# (default branch also has no CI history, e.g. device-imaging class), where burning
|
||||||
|
# the full timeout would be pure waste. There, NO_CI_MAX empty polls => fast-exit 0.
|
||||||
|
#
|
||||||
|
# Probe failure is treated conservatively as REPO_HAS_CI=1 (assume CI present): we
|
||||||
|
# would rather wait-then-timeout than risk a false-green, per the merge-gate priority.
|
||||||
|
REPO_HAS_CI=1
|
||||||
|
detect_repo_ci() {
|
||||||
|
local def_branch def_status
|
||||||
|
# Every early exit returns 0: a probe miss must leave the conservative
|
||||||
|
# REPO_HAS_CI=1 default in place, never abort the caller under `set -e`.
|
||||||
|
if [[ "$PLATFORM" == "github" ]]; then
|
||||||
|
def_branch=$(github_get_default_branch 2>/dev/null) || {
|
||||||
|
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
|
||||||
|
[[ -n "$def_branch" ]] || return 0
|
||||||
|
def_status=$(github_get_commit_status_json "$OWNER" "$REPO" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
|
||||||
|
else
|
||||||
|
def_branch=$(gitea_get_default_branch "$HOST" "$OWNER/$REPO" "$TOKEN" 2>/dev/null) || {
|
||||||
|
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
|
||||||
|
[[ -n "$def_branch" ]] || return 0
|
||||||
|
def_status=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
|
||||||
|
fi
|
||||||
|
if [[ "$def_status" == "no-status" || -z "$def_status" ]]; then
|
||||||
|
REPO_HAS_CI=0
|
||||||
|
echo "[pr-ci-wait] default branch '${def_branch}' has no CI status history — treating repo as CI-less (empty-poll fast-exit enabled)."
|
||||||
|
else
|
||||||
|
REPO_HAS_CI=1
|
||||||
|
echo "[pr-ci-wait] default branch '${def_branch}' has CI history (state=${def_status}) — repo runs CI; empty status on PR head => awaiting registration, will not fast-green."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
detect_repo_ci || true
|
||||||
|
|
||||||
NO_CI_STREAK=0
|
NO_CI_STREAK=0
|
||||||
NO_CI_MAX=3
|
NO_CI_MAX=3
|
||||||
|
|
||||||
@@ -285,10 +338,21 @@ while true; do
|
|||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
no-status)
|
no-status)
|
||||||
NO_CI_STREAK=$((NO_CI_STREAK + 1))
|
if [[ "$REPO_HAS_CI" == "1" ]]; then
|
||||||
if (( NO_CI_STREAK >= NO_CI_MAX )); then
|
# PRIMARY tier: repo demonstrably runs CI but this commit's pipeline
|
||||||
echo "[INFO] no CI configured for this repo/commit (PR #$PR_NUMBER, ${NO_CI_STREAK} consecutive empty polls); treating as green."
|
# has not registered yet (webhook/queue lag). Do NOT fast-green — keep
|
||||||
exit 0
|
# polling until it registers or the timeout fires. Reset the streak so
|
||||||
|
# a later genuine CI-less misread can't accumulate across this state.
|
||||||
|
NO_CI_STREAK=0
|
||||||
|
echo "[pr-ci-wait] empty status on PR head but repo runs CI — awaiting pipeline registration (webhook lag), not fast-greening."
|
||||||
|
else
|
||||||
|
# SECONDARY tier: genuinely CI-less repo (default branch has no CI
|
||||||
|
# history either). Empty polls => fast-exit green after NO_CI_MAX.
|
||||||
|
NO_CI_STREAK=$((NO_CI_STREAK + 1))
|
||||||
|
if (( NO_CI_STREAK >= NO_CI_MAX )); then
|
||||||
|
echo "[INFO] no CI configured for this repo/commit (PR #$PR_NUMBER, ${NO_CI_STREAK} consecutive empty polls, default branch also CI-less); treating as green."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
sleep "$INTERVAL_SEC"
|
sleep "$INTERVAL_SEC"
|
||||||
;;
|
;;
|
||||||
|
|||||||
Reference in New Issue
Block a user