feat(builder): query CI via project status script
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.
This commit is contained in:
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Query Gitea Actions for an exact SHA via curl. Output STATUS= keys.
|
||||
# Auth: GITEA_TOKEN in the environment or .env.builder. GITEA_API_URL optional.
|
||||
|
||||
sha=
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--sha) sha=$2; shift 2 ;;
|
||||
*) echo "Usage: ci-status.sh --sha SHA" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$sha" ]] || { echo "Usage: ci-status.sh --sha SHA" >&2; exit 2; }
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "Error: curl is required" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
root=${BUILDER_PROJECT_ROOT:-$(git rev-parse --show-toplevel)}
|
||||
origin=$(git -C "$root" remote get-url origin 2>/dev/null || true)
|
||||
[[ -n "$origin" ]] || { echo "Error: no origin remote" >&2; exit 3; }
|
||||
|
||||
read -r host path base <<<"$(WAIT_CI_ORIGIN=$origin python3 -c '
|
||||
import os, re
|
||||
url = os.environ["WAIT_CI_ORIGIN"].strip()
|
||||
url = re.sub(r"^git@([^:]+):", r"ssh://git@\1/", url)
|
||||
url = re.sub(r"^ssh://git@([^/]+):", r"ssh://git@\1/", url)
|
||||
if re.match(r"^https?://", url):
|
||||
m = re.match(r"^(https?)://([^/]+)(?:/(.*))?$", url)
|
||||
scheme, host, path = (m.group(1), m.group(2), m.group(3) or "") if m else ("", "", "")
|
||||
else:
|
||||
m = re.match(r"^ssh://git@([^/]+)/(.+)$", url)
|
||||
scheme, host, path = ("https", m.group(1), m.group(2)) if m else ("", "", "")
|
||||
path = re.sub(r"\.git$", "", path)
|
||||
print(host, path, (scheme or "https") + "://" + host)
|
||||
')"
|
||||
api=${GITEA_API_URL:-$base/api/v1}
|
||||
api=${api%/}
|
||||
[[ "$api" == */api/v1 ]] || api+=/api/v1
|
||||
repo=$(WAIT_CI_PATH=$path python3 -c 'import os,urllib.parse; print(urllib.parse.quote(os.environ["WAIT_CI_PATH"], safe="/"))')
|
||||
|
||||
gitea_curl() {
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
curl -fsS --retry 2 --connect-timeout 10 --max-time 30 \
|
||||
--config <(printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN") "$1"
|
||||
else
|
||||
curl -fsS --retry 2 --connect-timeout 10 --max-time 30 "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
set +e
|
||||
runs=$(gitea_curl "${api}/repos/${repo}/actions/runs?head_sha=${sha}&limit=20&page=1")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: curl cannot read Gitea Actions for $path" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
python3 -c '
|
||||
import json, sys
|
||||
sha = sys.argv[1]
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
except json.JSONDecodeError:
|
||||
print("STATUS=error")
|
||||
raise SystemExit(0)
|
||||
if isinstance(data, dict) and data.get("message") and not (data.get("workflow_runs") or data.get("runs")):
|
||||
print("STATUS=error")
|
||||
raise SystemExit(0)
|
||||
runs = data.get("workflow_runs", data.get("runs", [])) if isinstance(data, dict) else data
|
||||
if not isinstance(runs, list) or not runs:
|
||||
print("STATUS=empty")
|
||||
raise SystemExit(0)
|
||||
matched = []
|
||||
for run in runs:
|
||||
head = run.get("head_sha") or run.get("headSha") or (run.get("head_commit") or {}).get("id")
|
||||
if head and head.lower() == sha.lower() and (run.get("id") or run.get("database_id")):
|
||||
matched.append(run)
|
||||
if not matched:
|
||||
print("STATUS=empty")
|
||||
raise SystemExit(0)
|
||||
run = matched[0]
|
||||
run_id = run.get("id") or run.get("database_id")
|
||||
url = run.get("html_url") or run.get("web_url") or run.get("url") or "-"
|
||||
status = str(run.get("status") or "").lower()
|
||||
conclusion = str(run.get("conclusion") or run.get("result") or "").lower()
|
||||
if conclusion in ("failure", "failed", "cancelled", "canceled", "skipped", "timed_out"):
|
||||
state = "failed"
|
||||
elif status in ("completed", "success") and conclusion in ("", "success", "succeeded"):
|
||||
state = "success"
|
||||
elif status == "completed" and conclusion:
|
||||
state = "failed"
|
||||
else:
|
||||
state = "running"
|
||||
print("STATUS=%s ID=%s PIPELINE=%s" % (state, run_id, url))
|
||||
' "$sha" <<<"$runs"
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Query GitHub Actions for an exact SHA via gh. Output STATUS= keys.
|
||||
|
||||
sha=
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--sha) sha=$2; shift 2 ;;
|
||||
*) echo "Usage: ci-status.sh --sha SHA" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$sha" ]] || { echo "Usage: ci-status.sh --sha SHA" >&2; exit 2; }
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "Error: gh is required" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
root=${BUILDER_PROJECT_ROOT:-$(git rev-parse --show-toplevel)}
|
||||
origin=$(git -C "$root" remote get-url origin 2>/dev/null || true)
|
||||
[[ -n "$origin" ]] || { echo "Error: no origin remote" >&2; exit 3; }
|
||||
repo=$(WAIT_CI_ORIGIN=$origin python3 -c '
|
||||
import os, re
|
||||
url = os.environ["WAIT_CI_ORIGIN"].strip()
|
||||
url = re.sub(r"^git@([^:]+):", r"ssh://git@\1/", url)
|
||||
url = re.sub(r"^ssh://git@([^/]+):", r"ssh://git@\1/", url)
|
||||
if re.match(r"^https?://", url):
|
||||
m = re.match(r"^https?://[^/]+(?:/(.*))?$", url)
|
||||
path = m.group(1) if m else ""
|
||||
else:
|
||||
m = re.match(r"^ssh://git@[^/]+/(.+)$", url)
|
||||
path = m.group(1) if m else ""
|
||||
print(re.sub(r"\.git$", "", path or ""))
|
||||
')
|
||||
|
||||
set +e
|
||||
raw=$(gh run list --repo "$repo" --commit "$sha" --limit 20 --json databaseId,status,conclusion,url,headSha)
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: gh cannot list runs for $repo" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
except json.JSONDecodeError:
|
||||
print("STATUS=error")
|
||||
raise SystemExit(0)
|
||||
if not data:
|
||||
print("STATUS=empty")
|
||||
raise SystemExit(0)
|
||||
r = data[0]
|
||||
status = str(r.get("status") or "").lower()
|
||||
conc = str(r.get("conclusion") or "").lower()
|
||||
if status == "completed" and conc == "success":
|
||||
state = "success"
|
||||
elif status == "completed":
|
||||
state = "failed"
|
||||
else:
|
||||
state = "running"
|
||||
print("STATUS=%s ID=%s PIPELINE=%s" % (state, r.get("databaseId") or "-", r.get("url") or "-"))
|
||||
' <<<"$raw"
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Query GitLab CI for an exact SHA via glab. Output STATUS= keys.
|
||||
|
||||
sha=
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--sha) sha=$2; shift 2 ;;
|
||||
*) echo "Usage: ci-status.sh --sha SHA" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$sha" ]] || { echo "Usage: ci-status.sh --sha SHA" >&2; exit 2; }
|
||||
|
||||
if ! command -v glab >/dev/null 2>&1; then
|
||||
echo "Error: glab is required" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
root=${BUILDER_PROJECT_ROOT:-$(git rev-parse --show-toplevel)}
|
||||
origin=$(git -C "$root" remote get-url origin 2>/dev/null || true)
|
||||
[[ -n "$origin" ]] || { echo "Error: no origin remote" >&2; exit 3; }
|
||||
path=$(WAIT_CI_ORIGIN=$origin python3 -c '
|
||||
import os, re
|
||||
url = os.environ["WAIT_CI_ORIGIN"].strip()
|
||||
url = re.sub(r"^git@([^:]+):", r"ssh://git@\1/", url)
|
||||
url = re.sub(r"^ssh://git@([^/]+):", r"ssh://git@\1/", url)
|
||||
if re.match(r"^https?://", url):
|
||||
m = re.match(r"^https?://[^/]+(?:/(.*))?$", url)
|
||||
path = m.group(1) if m else ""
|
||||
else:
|
||||
m = re.match(r"^ssh://git@[^/]+/(.+)$", url)
|
||||
path = m.group(1) if m else ""
|
||||
print(re.sub(r"\.git$", "", path or ""))
|
||||
')
|
||||
encoded=$(WAIT_CI_PATH=$path python3 -c 'import os,urllib.parse; print(urllib.parse.quote(os.environ["WAIT_CI_PATH"], safe=""))')
|
||||
|
||||
set +e
|
||||
raw=$(glab api "projects/${encoded}/pipelines?sha=${sha}&per_page=20")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: glab cannot list pipelines for $path" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
except json.JSONDecodeError:
|
||||
print("STATUS=error")
|
||||
raise SystemExit(0)
|
||||
if isinstance(data, dict) and data.get("message"):
|
||||
print("STATUS=error")
|
||||
raise SystemExit(0)
|
||||
if not isinstance(data, list) or not data:
|
||||
print("STATUS=empty")
|
||||
raise SystemExit(0)
|
||||
p = data[0]
|
||||
st = str(p.get("status") or "").lower()
|
||||
if st in ("success", "passed"):
|
||||
state = "success"
|
||||
elif st in ("failed", "canceled", "cancelled", "skipped"):
|
||||
state = "failed"
|
||||
elif st in ("",):
|
||||
state = "empty"
|
||||
else:
|
||||
state = "running"
|
||||
print("STATUS=%s ID=%s PIPELINE=%s" % (state, p.get("id") or "-", p.get("web_url") or "-"))
|
||||
' <<<"$raw"
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Query Gitea Actions for an exact SHA via tea. Output STATUS= keys.
|
||||
|
||||
sha=
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--sha) sha=$2; shift 2 ;;
|
||||
*) echo "Usage: ci-status.sh --sha SHA" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$sha" ]] || { echo "Usage: ci-status.sh --sha SHA" >&2; exit 2; }
|
||||
|
||||
if ! command -v tea >/dev/null 2>&1; then
|
||||
echo "Error: tea is required" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
root=${BUILDER_PROJECT_ROOT:-$(git rev-parse --show-toplevel)}
|
||||
origin=$(git -C "$root" remote get-url origin 2>/dev/null || true)
|
||||
[[ -n "$origin" ]] || { echo "Error: no origin remote" >&2; exit 3; }
|
||||
path=$(WAIT_CI_ORIGIN=$origin python3 -c '
|
||||
import os, re
|
||||
url = os.environ["WAIT_CI_ORIGIN"].strip()
|
||||
url = re.sub(r"^git@([^:]+):", r"ssh://git@\1/", url)
|
||||
url = re.sub(r"^ssh://git@([^/]+):", r"ssh://git@\1/", url)
|
||||
if re.match(r"^https?://", url):
|
||||
m = re.match(r"^https?://[^/]+(?:/(.*))?$", url)
|
||||
path = m.group(1) if m else ""
|
||||
else:
|
||||
m = re.match(r"^ssh://git@[^/]+/(.+)$", url)
|
||||
path = m.group(1) if m else ""
|
||||
print(re.sub(r"\.git$", "", path or ""))
|
||||
')
|
||||
[[ -n "$path" ]] || { echo "Error: cannot parse origin" >&2; exit 3; }
|
||||
|
||||
set +e
|
||||
runs=$(tea api "repos/${path}/actions/runs?head_sha=${sha}&limit=20")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: tea cannot read Actions for $path" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
python3 -c '
|
||||
import json, sys
|
||||
sha = sys.argv[1]
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
except json.JSONDecodeError:
|
||||
print("STATUS=error")
|
||||
raise SystemExit(0)
|
||||
if isinstance(data, dict) and data.get("message") and not (data.get("workflow_runs") or data.get("runs")):
|
||||
print("STATUS=error")
|
||||
raise SystemExit(0)
|
||||
runs = data.get("workflow_runs", data.get("runs", [])) if isinstance(data, dict) else data
|
||||
if not isinstance(runs, list) or not runs:
|
||||
print("STATUS=empty")
|
||||
raise SystemExit(0)
|
||||
matched = [run for run in runs if str(run.get("head_sha") or run.get("headSha") or "").lower() == sha.lower()]
|
||||
if not matched:
|
||||
print("STATUS=empty")
|
||||
raise SystemExit(0)
|
||||
run = matched[0]
|
||||
run_id = run.get("id") or run.get("database_id") or "-"
|
||||
url = run.get("html_url") or run.get("web_url") or run.get("url") or "-"
|
||||
status = str(run.get("status") or "").lower()
|
||||
conclusion = str(run.get("conclusion") or run.get("result") or "").lower()
|
||||
if conclusion in ("failure", "failed", "cancelled", "canceled", "skipped", "timed_out"):
|
||||
state = "failed"
|
||||
elif status in ("completed", "success") and conclusion in ("", "success", "succeeded"):
|
||||
state = "success"
|
||||
elif status == "completed" and conclusion:
|
||||
state = "failed"
|
||||
else:
|
||||
state = "running"
|
||||
print("STATUS=%s ID=%s PIPELINE=%s" % (state, run_id, url))
|
||||
' "$sha" <<<"$runs"
|
||||
@@ -11,8 +11,7 @@
|
||||
# DOCKER_REPOSITORY=
|
||||
# IMAGE_TAG=
|
||||
# PLATFORMS= # optional override; project ARCHES lives in makefile.builder
|
||||
# BUILDER_CI_FORGE= # optional: github, gitlab, or gitea; useful for custom hosts
|
||||
# GITEA_API_URL= # optional: defaults to https://<origin-host>/api/v1
|
||||
# GITEA_API_URL= # optional for the curl CI status script
|
||||
# GITEA_TOKEN= # optional token for private Gitea repositories
|
||||
# COPY_DEST= # fallback: local dir, or docker dest container:/absolute/dir
|
||||
# COPY_SCRIPT= # external/temporary executable override; project-local paths belong in makefile.builder
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
|
||||
ARCHES ?= amd64
|
||||
ARCH ?= $(firstword $(ARCHES))
|
||||
# local: make docker/deb on this machine. ci: obtain.sh waits for the
|
||||
# forge pipeline of an exact SHA, then inspects the artifact. Not a track.
|
||||
# local: make docker/deb on this machine. ci: obtain.sh polls
|
||||
# CI_STATUS_SCRIPT for an exact SHA, then inspects the artifact. Not a track.
|
||||
BUILD_VENUE ?= local
|
||||
CI_STATUS_SCRIPT ?= .pouch/builder/ci-status.sh
|
||||
ifeq ($(strip $(ARCHES)),)
|
||||
$(error ARCHES must not be empty)
|
||||
endif
|
||||
|
||||
Reference in New Issue
Block a user