#!/usr/bin/env bash
# Host-side supervisor for ONE ephemeral earningscall-development runner slot.
#
# Loop: mint a short-lived registration token from the fine-grained PAT (which
# stays here on the host, never inside the container) and launch ONE throwaway
# --ephemeral runner container. The container runs a single job and exits; we
# then mint a fresh token and start a new container.
#
# Each systemd template instance (ecd-runner@1, @2, ...) runs its own copy of
# this loop with a unique CONTAINER_NAME, giving N concurrent runner slots so
# parallel jobs (e.g. the 5-version test-dateparser matrix) don't all queue
# behind one slot.
set -uo pipefail

: "${GITHUB_PAT:?GITHUB_PAT required in /etc/ecd-runner/runner.env}"
: "${REPO:?REPO required}"
: "${RUNNER_LABELS:?RUNNER_LABELS required}"
IMAGE="${IMAGE:-ecd-runner:latest}"
# While this flag exists (touched by ecd-runner-cleanup.sh), don't launch new
# runner containers: the cleanup deregisters all idle runners and wipes the
# shared cache volume, so nothing may register or mount it until it's done.
# Lives on tmpfs so a reboot mid-cleanup can't leave the slots paused forever.
PAUSE_FLAG="${PAUSE_FLAG:-/run/ecd-runner-paused}"
# Unique per template instance so concurrent slots don't collide on the docker
# container name. Defaults keep the script runnable by hand outside systemd.
INSTANCE="${INSTANCE:-0}"
CONTAINER_NAME="${CONTAINER_NAME:-ecd-runner-${INSTANCE}}"
API="https://api.github.com/repos/${REPO}/actions/runners/registration-token"

cleanup() {
  echo "[supervisor ${INSTANCE}] stopping; removing container ${CONTAINER_NAME}"
  docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
  exit 0
}
trap cleanup INT TERM

while true; do
  if [[ -e "${PAUSE_FLAG}" ]]; then
    echo "[supervisor ${INSTANCE}] paused (${PAUSE_FLAG} present); rechecking in 10s"
    sleep 10
    continue
  fi

  echo "[supervisor ${INSTANCE}] minting registration token for ${REPO}..."
  # Parse the token with grep/sed rather than jq: on this host jq is the snap
  # build, whose AppArmor profile transition on exec is blocked by the unit's
  # NoNewPrivileges=true ("cannot change profile for the next exec call"). Plain
  # text parsing avoids any external profile-transitioning binary.
  # --connect-timeout/--max-time so a hung GitHub API call can't stall this slot
  # forever; a failed mint just falls through to the retry-sleep below.
  # Keep the HTTP status + error body (no -f) so the journal shows WHY a mint
  # failed — an expired PAT (401) looks very different from a network blip.
  # The Authorization header arrives via stdin (-H @-), never argv, so the PAT
  # is not exposed in /proc/*/cmdline to other users on the host.
  RESP="$(printf 'Authorization: Bearer %s\n' "${GITHUB_PAT}" | curl -sSL -X POST \
      --connect-timeout 10 --max-time 30 \
      -w $'\n%{http_code}' \
      -H @- \
      -H "Accept: application/vnd.github+json" \
      -H "X-GitHub-Api-Version: 2022-11-28" \
      "${API}")"
  HTTP_CODE="${RESP##*$'\n'}"
  BODY="${RESP%$'\n'*}"
  REG_TOKEN="$(grep -o '"token"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"${BODY}" | sed 's/.*"\([^"]*\)"$/\1/')"

  if [[ -z "${REG_TOKEN}" ]]; then
    echo "[supervisor ${INSTANCE}] failed to mint registration token (HTTP ${HTTP_CODE:-?}: $(tr -d '\n' <<<"${BODY}" | head -c 200)); retrying in 30s" >&2
    sleep 30
    continue
  fi

  RUNNER_NAME="ecd-hzdev-${INSTANCE}-$(date +%s)-${RANDOM}"
  echo "[supervisor ${INSTANCE}] launching ephemeral runner ${RUNNER_NAME}"

  # Clear any stale container left under this slot's name. docker run --rm only
  # cleans up on the container's OWN exit, so if a previous supervisor process
  # was killed (crash/OOM, or systemd Restart=always without a clean ExecStop)
  # the container is orphaned and still holds the name — every docker run below
  # would then fail with a name conflict and wedge this slot in the backoff loop
  # until a manual docker rm. This is a no-op on the normal path.
  docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true

  # Last-instant pause recheck: narrows the window where a cleanup that started
  # after the top-of-loop check could otherwise race this launch.
  if [[ -e "${PAUSE_FLAG}" ]]; then
    echo "[supervisor ${INSTANCE}] pause flag appeared before launch; discarding token and waiting"
    continue
  fi

  # Isolation: no docker socket, non-root user (from image), no new privileges.
  # The only mount is the non-secret ~/.cache volume (uv + pip caches, shared
  # across slots; both lock their caches so concurrent installs are safe) — the
  # container never sees host secrets or the host filesystem.
  # UV_LINK_MODE=copy: the cache volume and the job workdir are different
  # filesystems, so uv can't hardlink from cache into the interpreter — pin copy
  # mode to skip the failed hardlink attempt + warning.
  # The registration token is passed through the docker CLI's ENVIRONMENT
  # (value-less -e), not argv: /proc/*/cmdline is world-readable and the docker
  # process lives for the whole job, while /proc/*/environ is owner-only.
  RUN_START=${SECONDS}
  RUNNER_TOKEN="${REG_TOKEN}" \
  docker run --rm --name "${CONTAINER_NAME}" \
    --shm-size=1g \
    --security-opt no-new-privileges \
    -e REPO="${REPO}" \
    -e RUNNER_TOKEN \
    -e RUNNER_NAME="${RUNNER_NAME}" \
    -e RUNNER_LABELS="${RUNNER_LABELS}" \
    -e UV_LINK_MODE=copy \
    -v ecd-runner-cache:/home/runner/.cache \
    "${IMAGE}"
  RC=$?
  RAN_FOR=$((SECONDS - RUN_START))
  (( RC != 0 )) && echo "[supervisor ${INSTANCE}] runner container exited rc=${RC} (will loop)"

  # Back off only when the container FAILED almost immediately: a fast non-zero
  # exit means it never started properly (bad image/token/network), and a 2s
  # loop would mint a new registration token every 2s and hammer the GitHub
  # API. A fast rc=0 exit is a legitimately quick job (or a cancelled one) —
  # the agent exits 0 after completing work — so it re-registers right away.
  if (( RAN_FOR < 10 && RC != 0 )); then
    echo "[supervisor ${INSTANCE}] container failed after ${RAN_FOR}s (likely startup failure); backing off 30s"
    sleep 30
  else
    echo "[supervisor ${INSTANCE}] job cycle complete (ran ${RAN_FOR}s); looping"
    sleep 2
  fi
done
