9ba9b3da0f
When BUILD_VENUE=ci, init confirms tea/glab/gh/curl (or a custom command) and writes .pouch/builder/ci-status.sh. wait_ci.sh only polls that script and no longer guesses forge CLIs.
185 lines
4.8 KiB
Bash
Executable File
185 lines
4.8 KiB
Bash
Executable File
#!/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
|