#!/usr/bin/env bash # Convenience wrapper around infra/ci-runners.compose.yml. Documented # in infra/README.md → "CI runners" → "Convenience script". # # Hides the compose-file path on every command, automates the # pre-pull of the job images that ADR-0015 §"Job image pinning and # pre-pull" otherwise leaves to a manual step, and exposes the # "one-runner-at-a-time" rotation pattern as a single verb so the CI # pipeline is never starved during a config refresh. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_FILE="${SCRIPT_DIR}/ci-runners.compose.yml" # Runner service names defined in the compose file. Update when a # fourth runner is added (rare; documented in infra/README.md). RUNNERS=(runner-1 runner-2 runner-3) # Pre-pulled job images. The labels in ci-runners.compose.yml point at # these tags; act_runner with `force_pull: false` (per # infra/runner-config.yaml) refuses to start a job if the image is not # already cached locally. JOB_IMAGES=( "catthehacker/ubuntu:act-22.04" "catthehacker/ubuntu:full-22.04" ) # Seconds to wait between rotation steps. The runners do not expose a # Compose healthcheck; this is a conservative pause to let act_runner # finish its registration handshake before we touch the next one. ROTATE_WAIT_SECONDS=15 dc() { docker compose -f "$COMPOSE_FILE" "$@" } usage() { cat <<'USAGE' Usage: ./infra/ci-runners.sh [args] Commands: up [--prepull] Bring up the three runner containers. --prepull also fetches the act_runner job images (`catthehacker/ubuntu:act-22.04` and `:full-22.04`) on the host first — required because `runner-config.yaml` sets `container.force_pull: false`. down Stop and remove the runner containers. Persistent state under `data/runner-N/.runner` survives — the runners re-authenticate from their stored credentials on the next `up`. For a clean wipe (forces re-registration with a fresh Gitea token), invoke `docker compose -f ci-runners.compose.yml down -v` directly. restart Restart one runner. rotate Rolling restart of every runner (runner-1, wait, runner-2, wait, …). The pause between steps lets the previous runner finish its registration handshake before the next one goes down, so the CI pipeline always has at least N-1 runners online. status docker compose ps for the runner services. logs [runner] Follow logs (one runner or all of them). pull-images Pre-pull / refresh the act_runner job images on the host. Idempotent (Docker reports "Image is up to date" when nothing changed). help Show this help. [args...] Anything else is passed through to `docker compose -f ci-runners.compose.yml ...`. Examples: ./infra/ci-runners.sh up --prepull ./infra/ci-runners.sh rotate ./infra/ci-runners.sh logs runner-2 ./infra/ci-runners.sh restart runner-1 USAGE } require_args() { local verb="$1" local n="$2" local hint="$3" shift 3 if [[ $# -lt $n ]]; then echo "Error: '$verb' requires $hint." >&2 echo "Try: ./infra/ci-runners.sh help" >&2 exit 64 fi } assert_known_runner() { local name="$1" for r in "${RUNNERS[@]}"; do if [[ "$r" == "$name" ]]; then return 0 fi done echo "Error: unknown runner '$name'. Known: ${RUNNERS[*]}." >&2 exit 64 } pull_images() { echo "Pre-pulling job images on the runner host:" for img in "${JOB_IMAGES[@]}"; do echo " • $img" docker pull "$img" done } cmd="${1:-help}" [[ $# -gt 0 ]] && shift case "$cmd" in up) if [[ "${1:-}" == "--prepull" ]]; then pull_images shift fi dc up -d "$@" ;; down) dc down "$@" ;; restart) require_args restart 1 "a runner name (e.g. runner-1)" "$@" assert_known_runner "$1" dc restart "$1" ;; rotate) echo "Rolling restart of ${#RUNNERS[@]} runners — ${ROTATE_WAIT_SECONDS}s pause between each." for r in "${RUNNERS[@]}"; do echo echo "→ Restarting $r…" dc restart "$r" if [[ "$r" != "${RUNNERS[-1]}" ]]; then echo " Waiting ${ROTATE_WAIT_SECONDS}s before the next runner." sleep "$ROTATE_WAIT_SECONDS" fi done echo echo "✓ Rotation complete." ;; status | ps) dc ps "$@" ;; logs) dc logs -f "$@" ;; pull-images) pull_images ;; help | -h | --help | "") usage ;; *) # Pass-through. dc "$cmd" "$@" ;; esac