lilith-platform.live/scripts/run/ci.sh
Natalie 4503f86573 feat(ci): ct-forge CI doctor — ground-truth health from forge DB
The REST API (/actions/tasks) hides runs that fail at parse/dispatch time,
so it reported 0 runs while the forge DB held 151 failed + 8 stuck runs —
all from the same root cause: ZERO runners registered on ct-forge
(action_task=0, nothing ever executed a step).

- infrastructure/forge-ci-doctor.sh: DB-backed health check over ssh; lists
  runners, per-repo run-status breakdown, recent failures, dispatched-task
  count; RED/GREEN verdict + exit code. shellcheck-clean.
- scripts/run/ci.sh: wire ./run ci:doctor; fix broken ci:status (org was
  'lilith/', real ct-forge org is 'platform/'); ci:setup-host now points at
  the terraform ci-runners module instead of dead black.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:05:48 -04:00

162 lines
5.8 KiB
Bash

#!/bin/bash
# CI commands for lilith-platform.live
# Sourced by the top-level ./run script — do not execute directly.
# ROOT_DIR is set by the caller.
COMMAND="${1:-}"
shift || true
FORGEJO_URL="http://134.199.243.61:3000"
# ct-forge org is "platform" (NOT "lilith" — that was the black-forge org).
FORGEJO_REPO="platform/lilith-platform.live"
FORGEJO_API="${FORGEJO_URL}/api/v1"
# Personal access token — set in shell env or ~/.config/forgejo/token
FORGEJO_TOKEN="${FORGEJO_TOKEN:-$(cat "$HOME/.config/forgejo/token" 2>/dev/null || echo "")}"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
require_token() {
if [[ -z "$FORGEJO_TOKEN" ]]; then
echo "ERROR: FORGEJO_TOKEN is required." >&2
echo ""
echo " Get a token from: ${FORGEJO_URL}/user/settings/applications"
echo " Then either:"
echo " export FORGEJO_TOKEN=<token>"
echo " echo <token> > ~/.config/forgejo/token && chmod 600 ~/.config/forgejo/token"
exit 1
fi
}
forgejo_api() {
local method="$1"; shift
local path="$1"; shift
curl -sf \
-X "$method" \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
"${FORGEJO_API}${path}" \
"$@"
}
trigger_workflow() {
local workflow="$1"
local ref="${2:-main}"
require_token
echo "Triggering workflow: ${workflow} (ref: ${ref})..."
forgejo_api POST "/repos/${FORGEJO_REPO}/actions/workflows/${workflow}/dispatches" \
-d "{\"ref\": \"${ref}\"}" > /dev/null
echo " Triggered. Monitor at: ${FORGEJO_URL}/${FORGEJO_REPO}/actions"
echo ""
# Poll for the new run to appear and show its URL
sleep 2
local run_url
run_url="$(forgejo_api GET "/repos/${FORGEJO_REPO}/actions/runs?limit=1" 2>/dev/null \
| grep -o '"html_url":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "")"
if [[ -n "$run_url" ]]; then
echo " Latest run: ${run_url}"
fi
}
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
case "$COMMAND" in
ci:trigger:quinn)
trigger_workflow "deploy-quinn-www.yml" "${1:-main}"
;;
ci:trigger:admin)
trigger_workflow "deploy-quinn-admin.yml" "${1:-main}"
;;
ci:trigger:my)
trigger_workflow "deploy-quinn-my.yml" "${1:-main}"
;;
ci:trigger:data)
trigger_workflow "deploy-quinn-data.yml" "${1:-main}"
;;
ci:trigger:newsletter)
trigger_workflow "deploy-quinn-newsletter.yml" "${1:-main}"
;;
ci:status)
require_token
echo "Recent workflow runs for ${FORGEJO_REPO}:"
echo ""
_limit="${1:-15}"
forgejo_api GET "/repos/${FORGEJO_REPO}/actions/runs?limit=${_limit}" 2>/dev/null \
| python3 -c "import json,sys
runs=json.load(sys.stdin).get('workflow_runs',[])
limit=int(sys.argv[1]) if len(sys.argv)>1 else 15
if not runs:
print(' (no runs found)')
raise SystemExit(0)
print(' sha status workflow title')
print(' ' + '-'*78)
for r in runs[:limit]:
sha=(r.get('commit_sha') or '')[:7]
wf=r.get('workflow_id') or '?'
status=r.get('status') or '?'
title=(r.get('title') or '')[:42]
icon='✔' if status=='success' else ('✖' if status in ('failure','cancelled') else '⟳')
print(f' {icon} {sha:7} {status:10} {wf:32} {title}')
" "${_limit}" \
|| echo " (no runs found or API unavailable)"
echo ""
echo " Full list: ${FORGEJO_URL}/${FORGEJO_REPO}/actions"
;;
ci:logs)
_workflow="${1:?Usage: ./run ci:logs <workflow-name>}"
# e.g.: ./run ci:logs deploy-quinn-www.yml
require_token
echo "Fetching latest run for ${_workflow}..."
_run_id="$(forgejo_api GET "/repos/${FORGEJO_REPO}/actions/runs?limit=20" 2>/dev/null \
| grep -oP '"workflow_id":"'"${_workflow}"'[^"]*"|"id":\d+' \
| grep -B1 '"workflow_id"' | grep '"id"' | head -1 | grep -oP '\d+' || echo "")"
if [[ -z "$_run_id" ]]; then
echo "ERROR: no recent run found for ${_workflow}" >&2
exit 1
fi
echo " Run ID: ${_run_id}"
echo " Logs: ${FORGEJO_URL}/${FORGEJO_REPO}/actions/runs/${_run_id}"
;;
ci:doctor)
# Ground-truth CI/CD health from the ct-forge DB (sees parse/dispatch
# failures the REST API hides). No FORGEJO_TOKEN needed — reads via ssh.
bash "$ROOT_DIR/infrastructure/forge-ci-doctor.sh" "$@"
;;
ci:setup-host)
echo "ct-forge runners are provisioned via terraform, not on black."
echo "See: @cocottetech/infra/terraform/ci-runners/README.md"
echo " cd ~/Code/@projects/@cocottetech/infra/terraform/ci-runners"
echo " export TF_VAR_do_token=\"\$(cat ~/.vault/do_pat_cocotte)\""
echo " export TF_VAR_forge_pat=\"\$(cat ~/.vault/forge-admin-quinn.api-token)\""
echo " terraform init && terraform apply -var=runners=1"
exit 0
;;
*)
echo "Unknown CI command: $COMMAND"
echo ""
echo "CI commands:"
echo " ./run ci:trigger:quinn Trigger quinn.www deployment via Forgejo Actions"
echo " ./run ci:trigger:admin Trigger quinn.admin deployment"
echo " ./run ci:trigger:my Trigger quinn.my deployment"
echo " ./run ci:trigger:data Trigger quinn.data deployment"
echo " ./run ci:trigger:newsletter Trigger newsletter deployment"
echo " ./run ci:status Show recent workflow run statuses (API)"
echo " ./run ci:doctor [--repo R] Ground-truth CI health from ct-forge DB"
echo " ./run ci:logs <workflow.yml> Show URL for latest run logs"
echo " ./run ci:setup-host How to provision ct-forge runners (terraform)"
echo ""
echo " FORGEJO_TOKEN env var required for all ci:trigger/status/logs commands."
exit 1
;;
esac