Files
vh f014d5534a gitea-runner: stack + playbook for self-hosted Actions
Central runner on ana-docker (gitea is local; existing fleet tooling
already SSHes from there). Playbook is parameterized so future
site-local runners (nh3-docker, esh-docker-vm) drop in via --var
overrides instead of copy-paste.

Includes a workflow template for vh/task-board that calls the existing
deploy-task-board.yaml playbook — keeps the playbook as the single
source of truth for "how task-board is deployed", manual or automated.

Labels embed `:docker://node:20-bookworm-slim` schema; without it,
act_runner v0.6+ silently falls back to host-mode and runs job steps
inside the Alpine runner container (no apt/python/node), breaking any
real workflow. node:20-bookworm-slim is small + has git + node so
actions/checkout works out of the box.
2026-04-29 18:12:36 -07:00

140 lines
6.0 KiB
YAML

# Deploy a Gitea Actions self-hosted runner to a Docker host.
#
# We currently run ONE central runner on ana-docker (gitea is local
# there and SSH-from-there to the rest of the fleet already works).
# This playbook is parameterized so the same file can stand up
# site-local runners later without copy-paste — see
# stacks/gitea-runner/README.md "Topology" for when to do that.
#
# Usage:
# # Central runner (defaults match ana-docker)
# scripts/elway ana-docker --playbook playbooks/deploy-gitea-runner.yaml
#
# # Site-local runner — override identity at the CLI; the .env on
# # the host carries the registration token + image pin.
# scripts/elway nh3-docker --playbook playbooks/deploy-gitea-runner.yaml \
# --var runner_name=nh3-docker-runner \
# --var runner_labels=pfi-fleet,nh3-docker
#
# scripts/elway esh-docker-vm --playbook playbooks/deploy-gitea-runner.yaml \
# --var runner_name=esh-runner \
# --var runner_labels=pfi-fleet,esh
#
# Prereqs on the target host:
# - Docker + compose plugin
# - traefik-net network exists (external)
# - /opt/docker/{compose,conf} convention (any PFI host already has this)
#
# Prereqs in gitea (do once before the first run):
# - Generate a registration token at the appropriate scope
# (admin / org / repo). Paste into .env on the target host
# under GITEA_RUNNER_REGISTRATION_TOKEN before this playbook runs
# `docker compose up -d`. See stacks/gitea-runner/README.md.
vars:
compose_dir: /opt/docker/compose/gitea-runner
data_dir: /opt/docker/conf/gitea-runner/data
# Optional overrides — leave blank to take whatever's already in the
# host-side .env. Setting these here patches the .env in place,
# which is how site-local runners get unique names/labels without
# editing files on the host by hand.
runner_name: ""
runner_labels: ""
steps:
# ── host-side directory prep ────────────────────────────────────────
- name: Ensure compose dir exists
shell: mkdir -p {{ compose_dir }}
creates: "{{ compose_dir }}"
- name: Ensure data dir exists
shell: mkdir -p {{ data_dir }}
creates: "{{ data_dir }}"
# ── compose + config files ──────────────────────────────────────────
- name: Upload compose.yaml
upload:
src: stacks/gitea-runner/compose.yaml
dest: "{{ compose_dir }}/compose.yaml"
mode: "0644"
- name: Upload runner config.yaml
upload:
src: stacks/gitea-runner/conf/config.yaml
dest: "{{ data_dir }}/config.yaml"
mode: "0644"
- name: Seed .env from template (only if absent)
upload:
src: stacks/gitea-runner/.env.example
dest: "{{ compose_dir }}/.env"
mode: "0644"
when: "[ ! -f {{ compose_dir }}/.env ]"
# ── optional CLI-driven identity overrides ──────────────────────────
# If runner_name was passed as --var, patch the line in the host's
# .env so subsequent compose-up uses it. Idempotent: changed_when
# checks whether the value differs from what's already there.
- name: Patch GITEA_RUNNER_NAME in .env (when --var runner_name=)
shell: sed -i 's|^GITEA_RUNNER_NAME=.*|GITEA_RUNNER_NAME={{ runner_name }}|' {{ compose_dir }}/.env
when: '[ -n "{{ runner_name }}" ]'
changed_when: '[ -n "{{ runner_name }}" ] && ! grep -q "^GITEA_RUNNER_NAME={{ runner_name }}$" {{ compose_dir }}/.env'
- name: Patch GITEA_RUNNER_LABELS in .env (when --var runner_labels=)
shell: sed -i 's|^GITEA_RUNNER_LABELS=.*|GITEA_RUNNER_LABELS={{ runner_labels }}|' {{ compose_dir }}/.env
when: '[ -n "{{ runner_labels }}" ]'
changed_when: '[ -n "{{ runner_labels }}" ] && ! grep -q "^GITEA_RUNNER_LABELS={{ runner_labels }}$" {{ compose_dir }}/.env'
# ── safety: surface missing registration token before bring-up ──────
- name: Verify registration token is present (or runner is already registered)
# Either the .env carries a non-empty token (first run) or
# data_dir/.runner exists (already registered). If neither, the
# container will start but spin in a registration loop — fail
# fast with a useful message instead.
shell: |
if [ -f {{ data_dir }}/.runner ]; then
echo "already registered (.runner present)"
exit 0
fi
tok=$(grep -E '^GITEA_RUNNER_REGISTRATION_TOKEN=' {{ compose_dir }}/.env | cut -d= -f2-)
if [ -z "$tok" ]; then
echo "ERROR: no .runner cached and GITEA_RUNNER_REGISTRATION_TOKEN is empty in {{ compose_dir }}/.env"
echo "Generate one at https://gitea.phasefinal.com/-/admin/actions/runners and paste it into the .env, then re-run."
exit 1
fi
echo "registration token present"
changed_when: "false"
# ── bring up + wait for runner to come online ───────────────────────
- name: docker compose up -d
shell: cd {{ compose_dir }} && docker compose up -d
- name: Wait for runner to register / start polling
# The runner logs "Runner registered" on first start, then enters
# a polling loop ("Listening for tasks" / "polling"). Either is a
# success signal.
shell: |
for i in $(seq 1 30); do
if docker logs gitea-runner 2>&1 | grep -qE "Runner registered|Listening for tasks|poll|Starting runner"; then
exit 0
fi
sleep 2
done
echo "--- last 80 lines of gitea-runner logs ---"
docker logs gitea-runner --tail 80
exit 1
changed_when: "false"
verify:
- name: Container is running
shell: docker inspect gitea-runner --format '{{.State.Status}}' | grep -q running
changed_when: "false"
- name: Runner credentials are cached on disk
shell: test -f {{ data_dir }}/.runner
changed_when: "false"
- name: Runner can reach gitea (no TLS / auth errors in last 50 log lines)
shell: '! docker logs gitea-runner --tail 50 2>&1 | grep -qiE "tls|x509|unauthorized|forbidden|connection refused"'
changed_when: "false"