270 lines
6.3 KiB
Bash
Executable File
270 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Wait until the forge pipeline for an exact commit SHA finishes.
|
|
# Does not trigger a pipeline. Does not print tokens.
|
|
#
|
|
# Usage:
|
|
# wait_ci.sh --sha SHA [--root DIR] [--timeout SEC]
|
|
#
|
|
# Exit:
|
|
# 0 pipeline succeeded; prints "OK pipeline=<url> sha=<sha>"
|
|
# 1 no pipeline for this SHA, or a job failed/canceled
|
|
# 2 usage / bad arguments
|
|
# 3 forge CLI missing, or it cannot read this project
|
|
|
|
usage() {
|
|
sed -n '2,16p' "$0"
|
|
}
|
|
|
|
sha=
|
|
root=
|
|
timeout_sec=1200
|
|
poll_sec=10
|
|
appear_sec=90
|
|
|
|
while (($#)); do
|
|
case "$1" in
|
|
--sha) sha=$2; shift 2 ;;
|
|
--root) root=$2; shift 2 ;;
|
|
--timeout) 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")
|
|
|
|
origin=$(git -C "$root" remote get-url origin 2>/dev/null || true)
|
|
if [[ -z "$origin" ]]; then
|
|
echo "Error: no origin remote" >&2
|
|
exit 2
|
|
fi
|
|
|
|
python_json() {
|
|
python3 -c "$1"
|
|
}
|
|
|
|
parse_origin() {
|
|
python_json '
|
|
import os, re, sys
|
|
url = os.environ["WAIT_CI_ORIGIN"]
|
|
url = url.strip()
|
|
url = re.sub(r"^git@", "ssh://git@", url)
|
|
url = re.sub(r"^ssh://git@([^/]+):", r"ssh://git@\1/", url)
|
|
url = re.sub(r"^https?://", "ssh://git@", url)
|
|
# ssh://git@host/group/repo.git
|
|
m = re.match(r"ssh://git@([^/]+)/(.+?)(?:\.git)?$", url)
|
|
if not m:
|
|
print("", "", "")
|
|
raise SystemExit(0)
|
|
host, path = m.group(1), m.group(2)
|
|
kind = "github" if host == "github.com" or host.endswith(".github.com") else "gitlab"
|
|
print(kind, host, path)
|
|
'
|
|
}
|
|
|
|
export WAIT_CI_ORIGIN=$origin
|
|
read -r forge_kind forge_host forge_path <<<"$(parse_origin)"
|
|
if [[ -z "$forge_kind" || -z "$forge_path" ]]; then
|
|
echo "Error: cannot parse origin: $origin" >&2
|
|
exit 2
|
|
fi
|
|
|
|
gitlab_pipelines() {
|
|
local encoded
|
|
encoded=$(python_json 'import os,urllib.parse; print(urllib.parse.quote(os.environ["WAIT_CI_PATH"], safe=""))')
|
|
glab api "projects/${encoded}/pipelines?sha=${sha}&per_page=20"
|
|
}
|
|
|
|
gitlab_jobs() {
|
|
local encoded pid=$1
|
|
encoded=$(python_json 'import os,urllib.parse; print(urllib.parse.quote(os.environ["WAIT_CI_PATH"], safe=""))')
|
|
glab api "projects/${encoded}/pipelines/${pid}/jobs"
|
|
}
|
|
|
|
export WAIT_CI_PATH=$forge_path
|
|
|
|
if [[ "$forge_kind" == github ]]; then
|
|
if ! command -v gh >/dev/null 2>&1; then
|
|
echo "Error: gh is required to watch GitHub Actions for $forge_host" >&2
|
|
exit 3
|
|
fi
|
|
else
|
|
if ! command -v glab >/dev/null 2>&1; then
|
|
echo "Error: glab is required to watch GitLab CI for $forge_host" >&2
|
|
exit 3
|
|
fi
|
|
fi
|
|
|
|
started=$(date +%s)
|
|
first_seen=0
|
|
empty_since=$started
|
|
|
|
summarize_gitlab() {
|
|
python_json '
|
|
import json, os, sys
|
|
raw = sys.stdin.read()
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
print("error")
|
|
raise SystemExit(0)
|
|
if isinstance(data, dict) and data.get("message"):
|
|
print("auth", data.get("message", "")[:80].replace(" ", "_"))
|
|
raise SystemExit(0)
|
|
if not isinstance(data, list):
|
|
print("error")
|
|
raise SystemExit(0)
|
|
if not data:
|
|
print("empty")
|
|
raise SystemExit(0)
|
|
p = data[0]
|
|
print(p.get("id"), p.get("status"), p.get("web_url", ""))
|
|
'
|
|
}
|
|
|
|
summarize_jobs() {
|
|
python_json '
|
|
import json, sys
|
|
data = json.loads(sys.stdin.read())
|
|
parts = []
|
|
for j in data if isinstance(data, list) else []:
|
|
parts.append("%s=%s" % (j.get("name"), j.get("status")))
|
|
print(",".join(parts))
|
|
'
|
|
}
|
|
|
|
github_runs() {
|
|
gh run list --repo "$forge_path" --commit "$sha" --limit 20 \
|
|
--json databaseId,status,conclusion,url,headSha
|
|
}
|
|
|
|
summarize_github() {
|
|
python_json '
|
|
import json, sys
|
|
data = json.loads(sys.stdin.read())
|
|
if not data:
|
|
print("empty")
|
|
raise SystemExit(0)
|
|
r = data[0]
|
|
status = r.get("status")
|
|
conc = r.get("conclusion")
|
|
if status == "completed" and conc == "success":
|
|
st = "success"
|
|
elif status == "completed":
|
|
st = conc or "failed"
|
|
else:
|
|
st = status or "running"
|
|
print(r.get("databaseId"), st, r.get("url", ""))
|
|
'
|
|
}
|
|
|
|
probe_auth() {
|
|
if [[ "$forge_kind" == github ]]; then
|
|
local out rc
|
|
set +e
|
|
out=$(github_runs 2>&1)
|
|
rc=$?
|
|
set -e
|
|
if [[ $rc -ne 0 ]]; then
|
|
echo "Error: gh cannot list runs for $forge_path" >&2
|
|
echo "$out" | sed '/token/Id; /Bearer/d' >&2
|
|
exit 3
|
|
fi
|
|
return
|
|
fi
|
|
local out rc
|
|
set +e
|
|
out=$(gitlab_pipelines 2>&1)
|
|
rc=$?
|
|
set -e
|
|
if [[ $rc -ne 0 ]]; then
|
|
echo "Error: glab cannot list pipelines for $forge_path on $forge_host" >&2
|
|
echo "$out" | sed '/token/Id; /Bearer/d' >&2
|
|
exit 3
|
|
fi
|
|
local kind
|
|
kind=$(printf '%s' "$out" | summarize_gitlab | awk '{print $1}')
|
|
if [[ "$kind" == auth ]]; then
|
|
echo "Error: glab is authenticated but cannot read $forge_path" >&2
|
|
exit 3
|
|
fi
|
|
}
|
|
|
|
probe_auth
|
|
|
|
status_line() {
|
|
if [[ "$forge_kind" == github ]]; then
|
|
github_runs | summarize_github
|
|
else
|
|
gitlab_pipelines | summarize_gitlab
|
|
fi
|
|
}
|
|
|
|
while true; do
|
|
now=$(date +%s)
|
|
if (( now - started > timeout_sec )); then
|
|
echo "FAIL reason=timeout sha=$sha" >&2
|
|
exit 1
|
|
fi
|
|
line=$(status_line)
|
|
set -- $line
|
|
kind=${1:-empty}
|
|
|
|
if [[ "$kind" == empty ]]; then
|
|
if (( now - empty_since > appear_sec )); then
|
|
echo "FAIL reason=no_pipeline sha=$sha origin=$forge_host/$forge_path" >&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
|
|
fi
|
|
if [[ "$kind" == error || "$kind" == auth ]]; then
|
|
echo "Error: unexpected forge response for $forge_path" >&2
|
|
exit 3
|
|
fi
|
|
|
|
pid=$1
|
|
st=$2
|
|
url=$3
|
|
empty_since=$now
|
|
if [[ "$first_seen" -eq 0 ]]; then
|
|
echo "Watching pipeline $pid status=$st $url" >&2
|
|
first_seen=1
|
|
fi
|
|
|
|
case "$st" in
|
|
success)
|
|
echo "OK pipeline=$url sha=$sha id=$pid"
|
|
exit 0
|
|
;;
|
|
failed|canceled|cancelled|skipped)
|
|
jobs_s=
|
|
if [[ "$forge_kind" != github ]]; then
|
|
jobs_s=$(gitlab_jobs "$pid" | summarize_jobs || true)
|
|
fi
|
|
echo "FAIL reason=job_$st sha=$sha pipeline=$url jobs=$jobs_s" >&2
|
|
exit 1
|
|
;;
|
|
created|pending|running|waiting_for_resource|preparing|waiting|queued|in_progress|waiting_for_resource)
|
|
sleep "$poll_sec"
|
|
;;
|
|
*)
|
|
sleep "$poll_sec"
|
|
;;
|
|
esac
|
|
done
|