forked from heavy-duty/rig
366 lines
17 KiB
Bash
366 lines
17 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# rig forgejo-runner install — Forgejo Actions runner as a systemd service
|
||
|
|
# under an unprivileged user. Outbound-only (long-poll to the instance), no
|
||
|
|
# inbound ports. Convergent toward --instance: re-running against the instance
|
||
|
|
# the box is already on leaves it alone; a box registered to a DIFFERENT
|
||
|
|
# instance is refused, never silently restarted on the old one.
|
||
|
|
#
|
||
|
|
# The GitHub sibling (runner-install.sh) refuses Docker outright: it converges
|
||
|
|
# a fleet MACHINE, where `docker` group membership is root-equivalent and the
|
||
|
|
# blast radius is the machine. This command's home is a ci-box TENANT, where
|
||
|
|
# bootstrap-tenant.sh has already installed Docker and added the tenant user to
|
||
|
|
# the group, and where the blast radius is a disposable guest with no inbound
|
||
|
|
# path. Same trade, different machine, opposite answer — which is why this is a
|
||
|
|
# separate command and not a flag on that one.
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||
|
|
# shellcheck source=SCRIPTDIR/lib/forgejo-runner-config.sh
|
||
|
|
. "$HERE/lib/forgejo-runner-config.sh"
|
||
|
|
|
||
|
|
log() { printf 'rig-forgejo-runner: %s\n' "$*"; }
|
||
|
|
warn() { printf 'rig-forgejo-runner: WARNING: %s\n' "$*" >&2; }
|
||
|
|
die() { printf 'rig-forgejo-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||
|
|
|
||
|
|
# The default label map. `runs-on: ubuntu-latest` is what a workflow written
|
||
|
|
# for GitHub says, so it must mean something here or every workflow needs
|
||
|
|
# editing to migrate; catthehacker's image is the act/Forgejo ecosystem's
|
||
|
|
# stand-in for GitHub's runner image. `docker` is the lean second option.
|
||
|
|
#
|
||
|
|
# Both are `docker://` — jobs run in CONTAINERS on the box's own dockerd, not
|
||
|
|
# on the box itself. No docker-in-docker: the guide this came from stacks a
|
||
|
|
# privileged dind sidecar with a plaintext tcp://…:2375 daemon to isolate jobs
|
||
|
|
# from a shared CI server, and inside a box that boundary is already paid for.
|
||
|
|
DEFAULT_LABELS='ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,docker:docker://node:22-bookworm'
|
||
|
|
|
||
|
|
usage() {
|
||
|
|
cat <<'EOF'
|
||
|
|
usage: rig forgejo-runner install --instance <url> [options]
|
||
|
|
|
||
|
|
--instance <url> Forgejo instance the runner registers to (required),
|
||
|
|
e.g. https://forgejo.example.com
|
||
|
|
--version <pin> forgejo-runner release to install, e.g. 12.13.2
|
||
|
|
(default: the latest release, resolved at install
|
||
|
|
time). Pin it for a deterministic, auditable install.
|
||
|
|
--name <name> runner name (default: this host's hostname)
|
||
|
|
--labels <csv> runner labels; replaces the default. The default maps
|
||
|
|
ubuntu-latest and docker onto container images, so a
|
||
|
|
workflow written for GitHub runs unchanged.
|
||
|
|
--user <name> unprivileged service user (default: the tenant user
|
||
|
|
`ci` when it exists, else forgejo-runner; created if
|
||
|
|
absent; never root)
|
||
|
|
|
||
|
|
Installs forgejo-runner as a systemd service under an unprivileged user. The
|
||
|
|
runner is an agent, not a server: it long-polls the instance outbound and
|
||
|
|
receives jobs down that already-established connection, so it needs ZERO
|
||
|
|
inbound ports.
|
||
|
|
|
||
|
|
Jobs run in Docker containers on this box's own daemon. Inside a ci-box tenant
|
||
|
|
that daemon is already there — `rig bootstrap ci-box` installs it and puts the
|
||
|
|
tenant user in the `docker` group.
|
||
|
|
|
||
|
|
Provide the runner registration token via the FORGEJO_RUNNER_TOKEN env var or
|
||
|
|
the interactive prompt. Get one from the scope you want the runner to serve:
|
||
|
|
instance Site Administration > Actions > Runners > Create new Runner
|
||
|
|
org Org > Settings > Actions > Runners
|
||
|
|
repo Repo > Settings > Actions > Runners
|
||
|
|
The SCOPE IS THE TOKEN'S, not a flag here. It is consumed at registration and
|
||
|
|
never written to disk by rig.
|
||
|
|
|
||
|
|
Convergent toward --instance: re-running against the instance this box is
|
||
|
|
already on re-uses the binary, skips registration, and never asks for a token.
|
||
|
|
A box registered to a DIFFERENT instance is refused — take it off the old one
|
||
|
|
with `rig forgejo-runner remove` first.
|
||
|
|
EOF
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- args (validated before the root check, so errors are testable) ---------
|
||
|
|
INSTANCE=""
|
||
|
|
VERSION=""
|
||
|
|
RUNNER_NAME="$(hostname)"
|
||
|
|
LABELS="$DEFAULT_LABELS"
|
||
|
|
RUNNER_USER=""
|
||
|
|
while [ $# -gt 0 ]; do
|
||
|
|
case "$1" in
|
||
|
|
--instance)
|
||
|
|
[ $# -ge 2 ] || die "--instance needs a value" 2
|
||
|
|
INSTANCE="$2"; shift 2 ;;
|
||
|
|
--version)
|
||
|
|
[ $# -ge 2 ] || die "--version needs a value" 2
|
||
|
|
VERSION="$2"; shift 2 ;;
|
||
|
|
--name)
|
||
|
|
[ $# -ge 2 ] || die "--name needs a value" 2
|
||
|
|
RUNNER_NAME="$2"; shift 2 ;;
|
||
|
|
--labels)
|
||
|
|
[ $# -ge 2 ] || die "--labels needs a value" 2
|
||
|
|
LABELS="$2"; shift 2 ;;
|
||
|
|
--user)
|
||
|
|
[ $# -ge 2 ] || die "--user needs a value" 2
|
||
|
|
RUNNER_USER="$2"; shift 2 ;;
|
||
|
|
--repo)
|
||
|
|
# Named, not "unknown flag": everyone arrives here from `rig runner
|
||
|
|
# install --repo`, and the honest answer is that the argument does not
|
||
|
|
# exist on this forge rather than that it is misspelled.
|
||
|
|
[ $# -ge 2 ] && shift
|
||
|
|
die "--repo does not exist here: a Forgejo runner registers to an INSTANCE, and whether it serves that whole instance, one org, or one repo is a property of the registration TOKEN you mint in Forgejo's UI. Pass --instance <url> and mint the token at the scope you want." 2 ;;
|
||
|
|
-h|--help) usage; exit 0 ;;
|
||
|
|
*) die "unknown flag: $1" 2 ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
# --- validation ----------------------------------------------------------
|
||
|
|
[ -n "$INSTANCE" ] || die "--instance <url> is required" 2
|
||
|
|
case "$INSTANCE" in
|
||
|
|
https://*|http://*) ;;
|
||
|
|
*) die "--instance must be a URL with a scheme, e.g. https://forgejo.example.com (got: $INSTANCE)" 2 ;;
|
||
|
|
esac
|
||
|
|
# A path component would be a repo URL — the GitHub habit, and the one mistake
|
||
|
|
# that produces a runner registered somewhere subtly wrong rather than a clean
|
||
|
|
# failure. Refuse it by name.
|
||
|
|
case "${INSTANCE#*://}" in
|
||
|
|
*/*[!/]*) die "--instance takes the instance ROOT, not a repository URL: got ${INSTANCE}. Scope comes from the token, not the URL." 2 ;;
|
||
|
|
esac
|
||
|
|
VERSION="${VERSION#v}"
|
||
|
|
[ -n "$LABELS" ] || die "--labels must not be empty" 2
|
||
|
|
|
||
|
|
# The tenant user is the default when it is there: inside a ci-box the runner
|
||
|
|
# IS the tenant, and inventing a second service account beside it would leave
|
||
|
|
# the docker-group membership bootstrap-tenant.sh converged on the wrong user.
|
||
|
|
# Falls back to a dedicated account so this still works on a plain machine.
|
||
|
|
if [ -z "$RUNNER_USER" ]; then
|
||
|
|
if id -u ci >/dev/null 2>&1; then RUNNER_USER="ci"; else RUNNER_USER="forgejo-runner"; fi
|
||
|
|
fi
|
||
|
|
[ "$RUNNER_USER" != "root" ] || die "runner user must not be root" 2
|
||
|
|
|
||
|
|
# --- guards ----------------------------------------------------------------
|
||
|
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||
|
|
if [ -r /etc/os-release ]; then
|
||
|
|
# Sourced in a subshell: os-release defines VERSION (e.g. "13 (trixie)"),
|
||
|
|
# which would clobber this script's $VERSION.
|
||
|
|
# shellcheck source=/dev/null
|
||
|
|
OS_FAMILY="$(. /etc/os-release && printf '%s %s' "${ID:-}" "${ID_LIKE:-}")"
|
||
|
|
case "$OS_FAMILY" in
|
||
|
|
*debian*) ;;
|
||
|
|
*) warn "not a Debian-family system (${OS_FAMILY:-unknown}); proceeding anyway" ;;
|
||
|
|
esac
|
||
|
|
else
|
||
|
|
warn "cannot read /etc/os-release; proceeding anyway"
|
||
|
|
fi
|
||
|
|
command -v curl >/dev/null || die "curl is required (run rig bootstrap first)"
|
||
|
|
command -v systemctl >/dev/null || die "systemctl is required — this command installs the runner as a systemd service"
|
||
|
|
|
||
|
|
# --- is this box already registered somewhere else? --------------------------
|
||
|
|
# Before anything is prompted for, downloaded, or started: --instance must
|
||
|
|
# agree with what is already on the box. Everything below treats an existing
|
||
|
|
# .runner as "nothing to do" — right for the instance the box is already on,
|
||
|
|
# silently wrong for any other. See assert_runner_instance.
|
||
|
|
REG_PENDING=1
|
||
|
|
if id -u "$RUNNER_USER" >/dev/null 2>&1; then
|
||
|
|
USER_HOME="$(getent passwd "$RUNNER_USER" | cut -d: -f6)"
|
||
|
|
RUNNER_DIR="$USER_HOME/forgejo-runner"
|
||
|
|
assert_runner_instance "$RUNNER_DIR" "$INSTANCE" || exit 1
|
||
|
|
if [ -e "$RUNNER_DIR/.runner" ]; then
|
||
|
|
REG_PENDING=0
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- registration token — only when registration is actually pending -------
|
||
|
|
if [ "$REG_PENDING" -eq 1 ]; then
|
||
|
|
FORGEJO_RUNNER_TOKEN="${FORGEJO_RUNNER_TOKEN:-}"
|
||
|
|
# Prompt only on a tty: headless, a bare `read` dies under set -e with no
|
||
|
|
# message at all. Refuse loudly, naming the variable.
|
||
|
|
if [ -z "$FORGEJO_RUNNER_TOKEN" ]; then
|
||
|
|
[ -t 0 ] || die "FORGEJO_RUNNER_TOKEN is unset and stdin is not a tty — set FORGEJO_RUNNER_TOKEN to run unattended"
|
||
|
|
read -rsp "forgejo runner registration token: " FORGEJO_RUNNER_TOKEN || { echo; die "no registration token read (EOF) — set FORGEJO_RUNNER_TOKEN to run unattended"; }
|
||
|
|
echo
|
||
|
|
fi
|
||
|
|
[ -n "$FORGEJO_RUNNER_TOKEN" ] || die "empty registration token"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- user --------------------------------------------------------------------
|
||
|
|
if ! id -u "$RUNNER_USER" >/dev/null 2>&1; then
|
||
|
|
useradd --create-home --shell /bin/bash "$RUNNER_USER"
|
||
|
|
log "created user ${RUNNER_USER}"
|
||
|
|
else
|
||
|
|
log "user exists"
|
||
|
|
fi
|
||
|
|
USER_HOME="$(getent passwd "$RUNNER_USER" | cut -d: -f6)"
|
||
|
|
RUNNER_GROUP="$(id -gn "$RUNNER_USER")"
|
||
|
|
RUNNER_DIR="$USER_HOME/forgejo-runner"
|
||
|
|
BIN=/usr/local/bin/forgejo-runner
|
||
|
|
|
||
|
|
# The runner talks to dockerd over its socket, so it needs the group. In a
|
||
|
|
# ci-box bootstrap-tenant.sh already did this for the tenant user; on a plain
|
||
|
|
# machine, or for a --user that is not the tenant, it has not.
|
||
|
|
if getent group docker >/dev/null 2>&1; then
|
||
|
|
if id -nG "$RUNNER_USER" | tr ' ' '\n' | grep -qx docker; then
|
||
|
|
log "${RUNNER_USER} already in the docker group"
|
||
|
|
else
|
||
|
|
usermod -aG docker "$RUNNER_USER"
|
||
|
|
log "added ${RUNNER_USER} to the docker group"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
warn "no docker group on this box — jobs using docker:// labels will fail. Inside a ci-box, 'rig bootstrap ci-box' installs docker; elsewhere install it before running jobs."
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- download ----------------------------------------------------------------
|
||
|
|
# Forgejo publishes BARE BINARIES (not a tarball) with a .sha256 beside each
|
||
|
|
# one. Taking that checksum is nearly free and makes the install auditable —
|
||
|
|
# the same instinct as `coolify install`'s mandatory version pin.
|
||
|
|
if [ -x "$BIN" ]; then
|
||
|
|
log "forgejo-runner binary already present at ${BIN}; skipping download"
|
||
|
|
else
|
||
|
|
case "$(uname -m)" in
|
||
|
|
x86_64) ARCH="amd64" ;;
|
||
|
|
aarch64) ARCH="arm64" ;;
|
||
|
|
*) die "unsupported arch: $(uname -m)" ;;
|
||
|
|
esac
|
||
|
|
if [ -z "$VERSION" ]; then
|
||
|
|
# No pin given: resolve the latest release by following the redirect on
|
||
|
|
# the /releases/latest page — no API call, no token, no JSON to parse on
|
||
|
|
# a dependency-free box (install.sh's resolve_latest_tag idiom).
|
||
|
|
LATEST_URL="$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
|
||
|
|
https://code.forgejo.org/forgejo/runner/releases/latest)" \
|
||
|
|
|| die "could not resolve the latest forgejo-runner release"
|
||
|
|
VERSION="${LATEST_URL##*/}"
|
||
|
|
VERSION="${VERSION#v}"
|
||
|
|
case "$VERSION" in
|
||
|
|
""|*[!0-9.]*) die "could not parse a version from ${LATEST_URL}" ;;
|
||
|
|
esac
|
||
|
|
log "resolved latest forgejo-runner: ${VERSION}"
|
||
|
|
fi
|
||
|
|
ASSET="forgejo-runner-${VERSION}-linux-${ARCH}"
|
||
|
|
URL="https://code.forgejo.org/forgejo/runner/releases/download/v${VERSION}/${ASSET}"
|
||
|
|
WORKDIR="$(mktemp -d)"
|
||
|
|
cleanup() { rm -rf "$WORKDIR"; }
|
||
|
|
trap cleanup EXIT
|
||
|
|
log "downloading forgejo-runner ${VERSION} (${ARCH})"
|
||
|
|
curl -fsSL "$URL" -o "$WORKDIR/forgejo-runner" \
|
||
|
|
|| die "could not download ${URL}"
|
||
|
|
if curl -fsSL "${URL}.sha256" -o "$WORKDIR/forgejo-runner.sha256" 2>/dev/null; then
|
||
|
|
# The published .sha256 names the asset, not our temp path. Compare the
|
||
|
|
# digest itself rather than rewriting the file into sha256sum -c's format:
|
||
|
|
# one comparison, no parsing of a file we did not write.
|
||
|
|
WANT="$(tr -d '\r' < "$WORKDIR/forgejo-runner.sha256" | awk '{print $1}' | head -n1)"
|
||
|
|
GOT="$(sha256sum "$WORKDIR/forgejo-runner" | awk '{print $1}')"
|
||
|
|
[ -n "$WANT" ] || die "the published checksum for ${ASSET} is unreadable — refusing to install an unverified binary"
|
||
|
|
[ "$WANT" = "$GOT" ] \
|
||
|
|
|| die "checksum mismatch for ${ASSET}: published ${WANT}, downloaded ${GOT} — refusing to install"
|
||
|
|
log "checksum verified (${GOT})"
|
||
|
|
else
|
||
|
|
# A warning, not a refusal: rig should not become unable to install because
|
||
|
|
# upstream changed its asset layout. But it must be LOUD — an operator who
|
||
|
|
# needs a verified install has to know this one was not.
|
||
|
|
warn "no published .sha256 for ${ASSET} — installing WITHOUT checksum verification"
|
||
|
|
fi
|
||
|
|
install -m 0755 -o root -g root "$WORKDIR/forgejo-runner" "$BIN"
|
||
|
|
log "installed ${BIN}"
|
||
|
|
fi
|
||
|
|
INSTALLED_VER="$("$BIN" --version 2>/dev/null | head -n1)"
|
||
|
|
[ -n "$INSTALLED_VER" ] || die "${BIN} does not answer --version — the download landed but cannot run"
|
||
|
|
|
||
|
|
# --- register ----------------------------------------------------------------
|
||
|
|
# UPSTREAM MARKS `register` DEPRECATED (measured on v12.13.2: both `register`
|
||
|
|
# and `create-runner-file` carry "(deprecated)" in their help). It is chosen
|
||
|
|
# here anyway, deliberately, and this is the reasoning to revisit when it
|
||
|
|
# finally goes:
|
||
|
|
#
|
||
|
|
# - It still works. `daemon` reads the `.runner` this writes, resolves the
|
||
|
|
# instance from it, and connects — verified against a live instance, where
|
||
|
|
# a planted `.runner` got as far as "Unauthenticated: unregistered runner".
|
||
|
|
# The mechanism is intact; only the credential was fake.
|
||
|
|
# - The successor needs MORE than rig can honestly ask for at this layer:
|
||
|
|
# `daemon --url --uuid --token-url` requires the runner to already exist on
|
||
|
|
# the instance, so the operator would have to create it via API/UI and
|
||
|
|
# carry back a UUID. That is a second, differently-shaped credential dance
|
||
|
|
# for no gain today.
|
||
|
|
# - `register` writes a file `status` can read back. The successor's config
|
||
|
|
# lives in flags on a unit line, where "what is this box registered to" has
|
||
|
|
# no on-disk answer that is not just rig's own copy of what it was told.
|
||
|
|
#
|
||
|
|
# When upstream removes it: the shape becomes `daemon --url/--uuid`, the unit
|
||
|
|
# gains those flags, and forgejo-runner-config.sh's readers move to whatever
|
||
|
|
# holds the UUID. assert_runner_instance's contract survives either way — it
|
||
|
|
# asks about the instance, which both spellings record.
|
||
|
|
install -d -m 0755 -o "$RUNNER_USER" -g "$RUNNER_GROUP" "$RUNNER_DIR"
|
||
|
|
if [ -e "$RUNNER_DIR/.runner" ]; then
|
||
|
|
log "already registered; skipping registration"
|
||
|
|
else
|
||
|
|
log "registering runner ${RUNNER_NAME} against ${INSTANCE}"
|
||
|
|
(cd "$RUNNER_DIR" && runuser -u "$RUNNER_USER" -- env HOME="$USER_HOME" \
|
||
|
|
"$BIN" register --no-interactive \
|
||
|
|
--instance "$INSTANCE" --token "$FORGEJO_RUNNER_TOKEN" \
|
||
|
|
--name "$RUNNER_NAME" --labels "$LABELS") \
|
||
|
|
|| die "registration failed — check the token is a RUNNER registration token from ${INSTANCE} and has not been used already"
|
||
|
|
[ -e "$RUNNER_DIR/.runner" ] \
|
||
|
|
|| die "register reported success but wrote no ${RUNNER_DIR}/.runner"
|
||
|
|
fi
|
||
|
|
# EVERY run, registration or not: .runner holds the runner's own long-lived
|
||
|
|
# token, and a mode that drifted leaks it silently. See the lib.
|
||
|
|
forgejo_runner_secure "$RUNNER_DIR" "$RUNNER_USER" "$RUNNER_GROUP"
|
||
|
|
|
||
|
|
# rig records the labels beside the registration for `status` to read back.
|
||
|
|
# Box-local metadata, never a credential — the same note runner-install.sh
|
||
|
|
# writes, for the same reason.
|
||
|
|
printf '%s\n' "$LABELS" > "$RUNNER_DIR/.rig-labels"
|
||
|
|
chown "$RUNNER_USER:$RUNNER_GROUP" "$RUNNER_DIR/.rig-labels"
|
||
|
|
|
||
|
|
# --- service -------------------------------------------------------------
|
||
|
|
# Written by rig rather than shipped by upstream: forgejo-runner has no
|
||
|
|
# svc.sh, so there is no vendor unit to defer to (the GitHub sibling defers to
|
||
|
|
# actions/runner's). Converged like every file rig writes — cmp-guarded, so a
|
||
|
|
# re-run that changes nothing reloads nothing.
|
||
|
|
UNIT=/etc/systemd/system/forgejo-runner.service
|
||
|
|
UNIT_TMP="$(mktemp)"
|
||
|
|
cat > "$UNIT_TMP" <<EOF
|
||
|
|
[Unit]
|
||
|
|
Description=Forgejo Actions runner
|
||
|
|
Documentation=https://forgejo.org/docs/latest/admin/actions/
|
||
|
|
After=network-online.target docker.service
|
||
|
|
Wants=network-online.target
|
||
|
|
|
||
|
|
[Service]
|
||
|
|
Type=simple
|
||
|
|
User=${RUNNER_USER}
|
||
|
|
WorkingDirectory=${RUNNER_DIR}
|
||
|
|
ExecStart=${BIN} daemon
|
||
|
|
Restart=on-failure
|
||
|
|
RestartSec=10
|
||
|
|
# The runner supervises job containers on this box's docker socket; it is not
|
||
|
|
# a sandbox for them. These keep the DAEMON from being a soft target.
|
||
|
|
NoNewPrivileges=true
|
||
|
|
PrivateTmp=true
|
||
|
|
ProtectSystem=full
|
||
|
|
ProtectHome=read-only
|
||
|
|
ReadWritePaths=${RUNNER_DIR}
|
||
|
|
|
||
|
|
[Install]
|
||
|
|
WantedBy=multi-user.target
|
||
|
|
EOF
|
||
|
|
if ! cmp -s "$UNIT_TMP" "$UNIT" 2>/dev/null; then
|
||
|
|
install -m 0644 "$UNIT_TMP" "$UNIT"
|
||
|
|
systemctl daemon-reload
|
||
|
|
log "systemd unit written: ${UNIT}"
|
||
|
|
else
|
||
|
|
log "systemd unit already current"
|
||
|
|
fi
|
||
|
|
rm -f "$UNIT_TMP"
|
||
|
|
|
||
|
|
systemctl enable forgejo-runner >/dev/null 2>&1 || die "could not enable forgejo-runner.service"
|
||
|
|
systemctl restart forgejo-runner || die "could not start forgejo-runner.service — see 'journalctl -u forgejo-runner'"
|
||
|
|
|
||
|
|
# Assert the EFFECTIVE state, not systemctl's exit code: a unit that starts and
|
||
|
|
# immediately dies (bad token, unreachable instance) leaves restart succeeding
|
||
|
|
# and the runner absent. Settle briefly, then ask.
|
||
|
|
active=""
|
||
|
|
for _ in 1 2 3 4 5 6; do
|
||
|
|
if systemctl is-active forgejo-runner >/dev/null 2>&1; then active=1; break; fi
|
||
|
|
sleep 2
|
||
|
|
done
|
||
|
|
[ -n "$active" ] || die "forgejo-runner.service is not active after 12s — see 'journalctl -u forgejo-runner' (a bad token or an unreachable instance both land here)"
|
||
|
|
|
||
|
|
log "runner ${RUNNER_NAME} (${INSTALLED_VER}) installed and running"
|
||
|
|
log "labels: ${LABELS}"
|
||
|
|
log "verify it shows Idle under ${INSTANCE} > Site Administration > Actions > Runners"
|
||
|
|
log "this box needs no inbound ports — the runner polls the instance outbound"
|