Files
apf_portal/infra/ci-runners.sh
T
Julien Gautier 308f505f38
CI / commits (pull_request) Successful in 1m36s
CI / scan (pull_request) Successful in 1m38s
CI / check (pull_request) Successful in 1m43s
CI / a11y (pull_request) Successful in 1m36s
CI / perf (pull_request) Successful in 3m18s
feat(infra): add ci-runners.sh wrapper for the runner stack
Mirrors the convenience-script pattern we adopted for
infra/local/dev.sh: typing
`docker compose -f infra/ci-runners.compose.yml ...` for routine
ops (status, restart, log tail) gets old fast, the pre-pull of
the catthehacker job images is documented but easy to forget,
and the "rotation of one runner at a time" tip in
infra/README.md is a sequence the contributor was supposed to
hand-roll every time.

`infra/ci-runners.sh` exposes:

  ./ci-runners.sh up                  # bring up runner-1..3
  ./ci-runners.sh up --prepull        # pre-pull job images first
  ./ci-runners.sh down                # stop + remove (preserves
                                       data/runner-N/.runner creds)
  ./ci-runners.sh restart <runner>    # restart one runner
  ./ci-runners.sh rotate              # rolling restart with a 15 s
                                       pause between each — keeps
                                       at least N-1 runners online
                                       through a config refresh
  ./ci-runners.sh status              # docker compose ps
  ./ci-runners.sh logs [runner]       # follow logs
  ./ci-runners.sh pull-images         # idempotent image refresh

Anything else is passed through to
`docker compose -f ci-runners.compose.yml ...`. The destructive
`down -v` (wipes data/, forces re-registration with a fresh
Gitea token) is intentionally NOT exposed as a verb — invoke
docker compose directly so the path is explicit at the typing
level.

`infra/README.md` updated:

- Inventory table at the top picks up the script.
- "First-time registration" walkthrough swaps the explicit
  docker pull / docker compose up steps for `./ci-runners.sh
  up --prepull`.
- New "Convenience script — ci-runners.sh" subsection with the
  cheat-sheet table.
- "Operational tips" rephrased to point at the script's `rotate`
  / `restart` / `logs` verbs as the canonical commands; the
  raw-docker-compose form is kept in parentheses as the
  underlying mechanism.
- "Adding a fourth runner" tip now reminds to update the
  RUNNERS=() array at the top of the script.

The 15 s pause in `rotate` is a conservative approximation —
act_runner doesn't expose a Compose healthcheck, so we can't
poll for ready. Adjust the constant if reality argues for a
different value.

Smoke-tested help + arg-validation paths on the host.
2026-05-10 06:33:50 +02:00

170 lines
4.9 KiB
Bash
Executable File

#!/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 <command> [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 <runner> 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.
<other> [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