#!/usr/bin/env bash set -euo pipefail # Poll the project CI status script until the exact SHA finishes. # Does not trigger a pipeline. Does not print tokens. # # Usage: # wait_ci.sh --sha SHA [--root DIR] [--timeout SEC] # # The project script is CI_STATUS_SCRIPT (makefile.builder, default # .pouch/builder/ci-status.sh). See references/ci-status.md. usage() { sed -n '2,14p' "$0" } sha= root= timeout_sec=1200 poll_sec=${BUILDER_CI_POLL_SEC:-10} appear_sec=${BUILDER_CI_APPEAR_SEC:-90} while (($#)); do case "$1" in --sha) [[ $# -ge 2 ]] || { echo "Error: --sha needs a value" >&2; exit 2; }; sha=$2; shift 2 ;; --root) [[ $# -ge 2 ]] || { echo "Error: --root needs a value" >&2; exit 2; }; root=$2; shift 2 ;; --timeout) [[ $# -ge 2 ]] || { echo "Error: --timeout needs a value" >&2; exit 2; }; timeout_sec=$2; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Error: unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done if [[ -z "$root" ]]; then root=$(git rev-parse --show-toplevel 2>/dev/null || true) fi if [[ -z "$root" || ! -d "$root/.git" && ! -f "$root/.git" ]]; then echo "Error: --root must be a git project" >&2 exit 2 fi if [[ -z "$sha" ]]; then echo "Error: --sha is required" >&2 exit 2 fi sha=$(git -C "$root" rev-parse "$sha") if [[ -f "$root/.env.builder" ]]; then while IFS='=' read -r key value; do key=${key%%[[:space:]]*} [[ -z "$key" || "$key" == \#* ]] && continue [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue if [[ -n "${!key:-}" ]]; then continue fi value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'} [[ -z "$value" ]] && continue printf -v "$key" '%s' "$value" export "$key" done < <(grep -v '^[[:space:]]*$' "$root/.env.builder") fi resolve_status_script() { local configured=${CI_STATUS_SCRIPT:-} if [[ -z "$configured" && -f "$root/makefile.builder" ]]; then configured=$(make -s --no-print-directory -C "$root" -f makefile.builder print-CI_STATUS_SCRIPT 2>/dev/null | tail -n 1 || true) fi configured=${configured:-.pouch/builder/ci-status.sh} local path if [[ "$configured" == /* ]]; then path=$configured else path=$root/$configured case "$configured" in .pouch/builder/*) ;; *) echo "Error: relative CI_STATUS_SCRIPT must be under .pouch/builder/: $configured" >&2 exit 2 ;; esac fi if [[ ! -f "$path" ]]; then echo "Error: CI status script missing: $path" >&2 echo "Run builder init with BUILD_VENUE=ci to write .pouch/builder/ci-status.sh" >&2 exit 2 fi if [[ ! -x "$path" ]]; then echo "Error: CI status script is not executable: $path" >&2 exit 2 fi printf '%s' "$path" } parse_status() { python3 -c ' import sys text = sys.stdin.read() values = {} for raw in text.replace("\n", " ").split(): if "=" in raw: key, value = raw.split("=", 1) values[key.upper()] = value print(values.get("STATUS", "error").lower()) print(values.get("ID", "-")) print(values.get("PIPELINE", "-")) print(values.get("JOBS", "")) ' } status_script=$(resolve_status_script) export BUILDER_PROJECT_ROOT=$root query() { local out err rc err=$(mktemp) set +e out=$(BUILDER_PROJECT_ROOT="$root" "$status_script" --sha "$sha" 2>"$err") rc=$? set -e if [[ $rc -ne 0 ]]; then echo "Error: CI status script exited $rc" >&2 sed '/token/Id; /Bearer/d; /Authorization/Id' "$err" >&2 rm -f "$err" if [[ $rc -eq 2 ]]; then exit 2 fi exit 3 fi rm -f "$err" printf '%s' "$out" } started=$(date +%s) first_seen=0 empty_since=$started while true; do now=$(date +%s) if (( now - started > timeout_sec )); then echo "FAIL reason=timeout sha=$sha" >&2 exit 1 fi raw=$(query) mapfile -t fields < <(printf '%s' "$raw" | parse_status) st=${fields[0]:-error} pid=${fields[1]:--} url=${fields[2]:--} jobs_s=${fields[3]:-} case "$st" in empty) if (( now - empty_since >= appear_sec )); then echo "FAIL reason=no_pipeline sha=$sha" >&2 echo "This SHA has no CI pipeline. Tag or push a ref the project actually builds." >&2 exit 1 fi sleep "$poll_sec" continue ;; error) echo "Error: unexpected CI status response" >&2 echo "$raw" | sed '/token/Id; /Bearer/d; /Authorization/Id' >&2 exit 3 ;; success) echo "OK pipeline=$url sha=$sha id=$pid jobs=$jobs_s" exit 0 ;; failed|failure|canceled|cancelled|skipped) echo "FAIL reason=job_${st} sha=$sha pipeline=$url jobs=$jobs_s" >&2 exit 1 ;; running|pending|queued|waiting|in_progress|created|preparing) if [[ "$first_seen" -eq 0 ]]; then echo "Watching pipeline $pid status=$st $url" >&2 first_seen=1 fi empty_since=$now sleep "$poll_sec" ;; *) empty_since=$now sleep "$poll_sec" ;; esac done