diff --git a/packages/mosaic/framework/tools/git/pr-ci-wait.sh b/packages/mosaic/framework/tools/git/pr-ci-wait.sh index 75d2b46..1a47759 100755 --- a/packages/mosaic/framework/tools/git/pr-ci-wait.sh +++ b/packages/mosaic/framework/tools/git/pr-ci-wait.sh @@ -147,6 +147,21 @@ gitea_get_commit_status_json() { 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 case "$1" in -n|--number) @@ -250,10 +265,48 @@ else exit 1 fi -# Count consecutive polls that find NO pipeline/status at all. A repo/commit with -# 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 -# with a clear "no CI configured" message — distinct from a real failure. +# No-CI determination is TWO-TIER (primary: CI history; secondary: empty-poll streak). +# +# PRIMARY — "does this repo run CI at all?" Probed once, up front, from the DEFAULT +# 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_MAX=3 @@ -285,10 +338,21 @@ while true; do exit 1 ;; no-status) - 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); treating as green." - exit 0 + if [[ "$REPO_HAS_CI" == "1" ]]; then + # PRIMARY tier: repo demonstrably runs CI but this commit's pipeline + # has not registered yet (webhook/queue lag). Do NOT fast-green — keep + # 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 sleep "$INTERVAL_SEC" ;;