Compare commits

..

1 Commits

Author SHA1 Message Date
Mos (Hermes)
a4cbd4be51 fix(tools/git/pr-ci-wait): stdin collision in python3 - <<PY made wrapper always return "unknown"
When the wrapper invoked `python3 - <<'PY' ... PY` inside a function that
was being fed JSON via a pipe (`printf '%s' "$STATUS_JSON" |
extract_state_from_status_json`), the heredoc bound stdin to the Python
program text. The `-` argument tells Python to read its program from
stdin, so the program consumed stdin before json.load(sys.stdin) ran —
which then saw EOF and bailed to the "unknown" branch every time.

Result: pr-ci-wait.sh hung the full timeout (default 30 min) even when
the upstream Gitea status was already 'success', because every poll
returned 'unknown' and the loop kept retrying.

Fix: capture the piped JSON into a local variable with `payload=$(cat)`
BEFORE invoking python, then pass it via env (PR_CI_STATUS_JSON). The
heredoc still drives the Python program, but the payload is now read
from the environment instead of a stdin that's already been consumed.

Same fix applied to print_status_summary() which has the identical
pattern.

Verified locally:
  $ echo '{"state":"success"}' | extract_state_from_status_json → success
  $ echo '' | extract_state_from_status_json → unknown
  $ echo '{"state":null,"statuses":[{"state":"success"}]}' | … → success
  $ echo '{"state":"pending"}' | extract_state_from_status_json → pending
  $ echo '{"state":"failure"}' | extract_state_from_status_json → failure

Reported in screenshot from operator session 2026-05-13 — wrapper was
stuck waiting on a PR whose underlying Gitea status was already
success. Operator workaround was to bypass the wrapper and use the raw
Gitea API.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:30:52 -05:00
3 changed files with 24 additions and 37 deletions

View File

@@ -52,20 +52,6 @@ _mosaic_sync_woodpecker_env() {
printf '%s\n' "$expected" > "$env_file" printf '%s\n' "$expected" > "$env_file"
} }
# Load legacy flat Woodpecker credentials (.woodpecker.url / .woodpecker.token).
# Some environments export WOODPECKER_INSTANCE=mosaic, but the current
# credentials.json may still use the legacy flat schema. Treat "mosaic" as the
# default flat instance when a nested .woodpecker.mosaic object is absent.
_mosaic_load_woodpecker_legacy() {
export WOODPECKER_URL="$(_mosaic_read_cred '.woodpecker.url')"
export WOODPECKER_TOKEN="$(_mosaic_read_cred '.woodpecker.token')"
export WOODPECKER_INSTANCE="${WOODPECKER_INSTANCE:-mosaic}"
WOODPECKER_URL="${WOODPECKER_URL%/}"
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
_mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
}
load_credentials() { load_credentials() {
local service="$1" local service="$1"
@@ -169,14 +155,7 @@ EOF
;; ;;
woodpecker-*) woodpecker-*)
local wp_instance="${service#woodpecker-}" local wp_instance="${service#woodpecker-}"
# credentials.json is authoritative — always read from it, ignore env. # credentials.json is authoritative — always read from it, ignore env
# Backward compatibility: the default Mosaic Woodpecker instance may be
# stored in the legacy flat schema (.woodpecker.url/.token) instead of
# .woodpecker.mosaic.url/.token.
if [[ "$wp_instance" == "mosaic" ]] && [[ -z "$(_mosaic_read_cred '.woodpecker.mosaic.url')" ]] && [[ -n "$(_mosaic_read_cred '.woodpecker.url')" ]]; then
WOODPECKER_INSTANCE="mosaic" _mosaic_load_woodpecker_legacy
return $?
fi
export WOODPECKER_URL="$(_mosaic_read_cred ".woodpecker.${wp_instance}.url")" export WOODPECKER_URL="$(_mosaic_read_cred ".woodpecker.${wp_instance}.url")"
export WOODPECKER_TOKEN="$(_mosaic_read_cred ".woodpecker.${wp_instance}.token")" export WOODPECKER_TOKEN="$(_mosaic_read_cred ".woodpecker.${wp_instance}.token")"
export WOODPECKER_INSTANCE="$wp_instance" export WOODPECKER_INSTANCE="$wp_instance"
@@ -187,10 +166,7 @@ EOF
_mosaic_sync_woodpecker_env "$wp_instance" "$WOODPECKER_URL" "$WOODPECKER_TOKEN" _mosaic_sync_woodpecker_env "$wp_instance" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
;; ;;
woodpecker) woodpecker)
# Resolve default instance, then load it. If WOODPECKER_INSTANCE is set to # Resolve default instance, then load it
# "mosaic" by a shell/profile but credentials.json still uses the legacy
# flat .woodpecker.url/.token schema, load the flat credentials instead of
# failing with "woodpecker.mosaic.url not found".
local wp_default local wp_default
wp_default="${WOODPECKER_INSTANCE:-$(_mosaic_read_cred '.woodpecker.default')}" wp_default="${WOODPECKER_INSTANCE:-$(_mosaic_read_cred '.woodpecker.default')}"
if [[ -z "$wp_default" ]]; then if [[ -z "$wp_default" ]]; then
@@ -198,18 +174,18 @@ EOF
local legacy_url local legacy_url
legacy_url="$(_mosaic_read_cred '.woodpecker.url')" legacy_url="$(_mosaic_read_cred '.woodpecker.url')"
if [[ -n "$legacy_url" ]]; then if [[ -n "$legacy_url" ]]; then
_mosaic_load_woodpecker_legacy export WOODPECKER_URL="${WOODPECKER_URL:-$legacy_url}"
export WOODPECKER_TOKEN="${WOODPECKER_TOKEN:-$(_mosaic_read_cred '.woodpecker.token')}"
WOODPECKER_URL="${WOODPECKER_URL%/}"
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
else else
echo "Error: woodpecker.default not set and no WOODPECKER_INSTANCE env var" >&2 echo "Error: woodpecker.default not set and no WOODPECKER_INSTANCE env var" >&2
echo "Available instances: $(jq -r '.woodpecker | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2 echo "Available instances: $(jq -r '.woodpecker | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2
return 1 return 1
fi fi
else else
if [[ "$wp_default" == "mosaic" ]] && [[ -z "$(_mosaic_read_cred '.woodpecker.mosaic.url')" ]] && [[ -n "$(_mosaic_read_cred '.woodpecker.url')" ]]; then load_credentials "woodpecker-${wp_default}"
WOODPECKER_INSTANCE="mosaic" _mosaic_load_woodpecker_legacy
else
load_credentials "woodpecker-${wp_default}"
fi
fi fi
;; ;;
cloudflare-*) cloudflare-*)

View File

@@ -30,12 +30,19 @@ EOF
# get_remote_host and get_gitea_token are provided by detect-platform.sh # get_remote_host and get_gitea_token are provided by detect-platform.sh
extract_state_from_status_json() { extract_state_from_status_json() {
python3 - <<'PY' # Capture piped JSON BEFORE invoking `python3 - <<PY`. The heredoc binds
# stdin to the Python program text — so json.load(sys.stdin) inside would
# try to re-read stdin after `-` already consumed it for the program,
# yielding EOF and returning "unknown" every time. Pass payload via env.
local payload
payload=$(cat)
PR_CI_STATUS_JSON="$payload" python3 - <<'PY'
import json import json
import os
import sys import sys
try: try:
payload = json.load(sys.stdin) payload = json.loads(os.environ.get("PR_CI_STATUS_JSON", ""))
except Exception: except Exception:
print("unknown") print("unknown")
raise SystemExit(0) raise SystemExit(0)
@@ -66,12 +73,16 @@ PY
} }
print_status_summary() { print_status_summary() {
python3 - <<'PY' # Same stdin-collision fix as extract_state_from_status_json above.
local payload
payload=$(cat)
PR_CI_STATUS_JSON="$payload" python3 - <<'PY'
import json import json
import os
import sys import sys
try: try:
payload = json.load(sys.stdin) payload = json.loads(os.environ.get("PR_CI_STATUS_JSON", ""))
except Exception: except Exception:
print("[pr-ci-wait] status payload unavailable") print("[pr-ci-wait] status payload unavailable")
raise SystemExit(0) raise SystemExit(0)

View File

@@ -50,7 +50,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
response=$(curl -sk -w "\n%{http_code}" \ response=$(curl -sk -w "\n%{http_code}" \
-H "Authorization: Bearer $WOODPECKER_TOKEN" \ -H "Authorization: Bearer $WOODPECKER_TOKEN" \
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?perPage=${LIMIT}") "${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?per_page=${LIMIT}")
http_code=$(echo "$response" | tail -n1) http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d') body=$(echo "$response" | sed '$d')