a24eee4c68
scripts/provision-gitea-runner.sh is a one-shot, idempotent host setup for an act_runner LXC. It mounts persistent host volumes for GOMODCACHE / GOCACHE / act-clones, pre-pulls the runner image, pre-clones the common GitHub actions, installs golangci-lint, and sets up a nightly cron to refresh the lot. Generic — no per-project state. With those persistent volumes in place, `cache: true` on actions/setup-go becomes a net negative — the action keeps tar-ing / un-tar-ing GOMODCACHE+GOCACHE through the Gitea cache backend on every job, adding ~10s per job and overwriting the volume contents. Drop it from all three jobs in ci.yml. Add a header comment block explaining the runner-side expectations and the Go version / build matrix / upload-artifact context for anyone reading later.
328 lines
12 KiB
Bash
Executable File
328 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# provision-gitea-runner.sh — one-shot, idempotent host setup for an
|
|
# act_runner LXC. Speeds up Gitea Actions runs by:
|
|
#
|
|
# 1. Disabling forced docker pulls (image refresh moves to a cron).
|
|
# 2. Mounting persistent host volumes for Go module/build caches and
|
|
# the act-actions clone cache.
|
|
# 3. Pre-pulling the runner-images container image.
|
|
# 4. Pre-cloning a configurable list of GitHub actions into the
|
|
# act cache so jobs don't fetch them on every run.
|
|
# 5. Installing golangci-lint (latest v2.x) at /usr/local/bin.
|
|
# 6. Setting up a nightly cron to refresh image + action clones +
|
|
# golangci-lint.
|
|
#
|
|
# The script is generic — no per-project state. Point it at any LXC
|
|
# running act_runner as a systemd service and it will provision the
|
|
# host. Re-runs are safe; they reconcile state.
|
|
#
|
|
# Usage: sudo ./provision-gitea-runner.sh
|
|
#
|
|
# Configurable via environment variables (defaults shown):
|
|
#
|
|
# CACHE_BASE=/var/cache/gitea-runner
|
|
# ACT_RUNNER_CONFIG=/etc/act_runner/config.yaml
|
|
# RUNNER_IMAGE=docker.gitea.com/runner-images:ubuntu-latest
|
|
# ACTIONS_TO_PRECLONE=(actions/checkout@v4 actions/setup-go@v5
|
|
# actions/upload-artifact@v4
|
|
# golangci/golangci-lint-action@v7)
|
|
#
|
|
# To add more pre-cloned actions later, edit /etc/cron.d/gitea-runner-refresh
|
|
# (the ACTIONS list is materialised into the cron script).
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------- defaults ---------------------------------------------------
|
|
|
|
: "${CACHE_BASE:=/var/cache/gitea-runner}"
|
|
: "${ACT_RUNNER_CONFIG:=/etc/act_runner/config.yaml}"
|
|
: "${RUNNER_IMAGE:=docker.gitea.com/runner-images:ubuntu-latest}"
|
|
|
|
DEFAULT_ACTIONS=(
|
|
"actions/checkout@v4"
|
|
"actions/setup-go@v5"
|
|
"actions/upload-artifact@v4"
|
|
"golangci/golangci-lint-action@v7"
|
|
)
|
|
# Allow caller to override by exporting ACTIONS_TO_PRECLONE as a
|
|
# space-separated string (env vars can't carry arrays cleanly).
|
|
if [[ -n "${ACTIONS_TO_PRECLONE:-}" ]]; then
|
|
read -r -a ACTIONS <<<"${ACTIONS_TO_PRECLONE}"
|
|
else
|
|
ACTIONS=("${DEFAULT_ACTIONS[@]}")
|
|
fi
|
|
|
|
# ---------- helpers ----------------------------------------------------
|
|
|
|
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m==>\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || die "missing: $1 (install it first)"
|
|
}
|
|
|
|
# sha256_url <url> — act_runner names its action-clone dirs after
|
|
# sha256(URL). Verified against a real run log:
|
|
# url=https://github.com/actions/checkout
|
|
# sha256=c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
|
|
sha256_url() {
|
|
printf '%s' "$1" | sha256sum | awk '{print $1}'
|
|
}
|
|
|
|
# ---------- pre-flight -------------------------------------------------
|
|
|
|
[[ $EUID -eq 0 ]] || die "run as root (the act_runner service writes /var/lib/act_runner as root)"
|
|
|
|
require_cmd systemctl
|
|
require_cmd docker
|
|
require_cmd git
|
|
require_cmd curl
|
|
require_cmd python3
|
|
|
|
# PyYAML for the config edit. Install if missing — Ubuntu 24.04 ships
|
|
# python3-yaml in the default repos.
|
|
if ! python3 -c 'import yaml' 2>/dev/null; then
|
|
log "installing python3-yaml (needed for safe YAML edits)"
|
|
apt-get update -qq
|
|
apt-get install -y -qq python3-yaml
|
|
fi
|
|
|
|
[[ -f "$ACT_RUNNER_CONFIG" ]] || die "$ACT_RUNNER_CONFIG not found — is act_runner installed?"
|
|
|
|
systemctl list-unit-files act_runner.service >/dev/null 2>&1 || \
|
|
die "act_runner.service not found — register the runner first"
|
|
|
|
log "pre-flight OK"
|
|
log " cache base : $CACHE_BASE"
|
|
log " config file : $ACT_RUNNER_CONFIG"
|
|
log " runner image : $RUNNER_IMAGE"
|
|
log " actions to clone : ${ACTIONS[*]}"
|
|
|
|
# ---------- 1. cache directories ---------------------------------------
|
|
|
|
log "creating cache directories under $CACHE_BASE"
|
|
for sub in go-mod go-build act-actions; do
|
|
install -d -m 0755 -o root -g root "$CACHE_BASE/$sub"
|
|
done
|
|
|
|
# ---------- 2. edit /etc/act_runner/config.yaml ------------------------
|
|
#
|
|
# Three keys are reconciled to known values:
|
|
#
|
|
# container.force_pull : false (we keep the image fresh via cron)
|
|
# container.options : "-v <cache mounts...>" (auto-mount caches
|
|
# into every job container)
|
|
# container.valid_volumes: [<our cache paths>] (whitelist so the
|
|
# container.options mounts are accepted)
|
|
#
|
|
# Other keys are preserved verbatim. The edit is idempotent: re-running
|
|
# yields the same file content.
|
|
|
|
log "patching $ACT_RUNNER_CONFIG"
|
|
|
|
# Backup once (only if no .pre-provision backup exists yet).
|
|
if [[ ! -f "${ACT_RUNNER_CONFIG}.pre-provision" ]]; then
|
|
cp -p "$ACT_RUNNER_CONFIG" "${ACT_RUNNER_CONFIG}.pre-provision"
|
|
log " saved pristine copy to ${ACT_RUNNER_CONFIG}.pre-provision"
|
|
fi
|
|
|
|
CONTAINER_OPTIONS_VALUE="-v ${CACHE_BASE}/go-mod:/root/go/pkg/mod:rw -v ${CACHE_BASE}/go-build:/root/.cache/go-build:rw -v ${CACHE_BASE}/act-actions:/root/.cache/act:rw"
|
|
|
|
CACHE_BASE="$CACHE_BASE" CONTAINER_OPTIONS_VALUE="$CONTAINER_OPTIONS_VALUE" \
|
|
ACT_RUNNER_CONFIG="$ACT_RUNNER_CONFIG" \
|
|
python3 - <<'PY'
|
|
import os, sys, yaml
|
|
cfg_path = os.environ['ACT_RUNNER_CONFIG']
|
|
cache_base = os.environ['CACHE_BASE']
|
|
container_options = os.environ['CONTAINER_OPTIONS_VALUE']
|
|
|
|
with open(cfg_path) as f:
|
|
cfg = yaml.safe_load(f) or {}
|
|
|
|
cfg.setdefault('container', {})
|
|
cfg['container']['force_pull'] = False
|
|
cfg['container']['options'] = container_options
|
|
|
|
# Whitelist every cache subdir explicitly so jobs that try to bind-mount
|
|
# them via workflow-side `volumes:` (rare but possible) are accepted.
|
|
desired_vols = [
|
|
f"{cache_base}/go-mod",
|
|
f"{cache_base}/go-build",
|
|
f"{cache_base}/act-actions",
|
|
]
|
|
existing = cfg['container'].get('valid_volumes') or []
|
|
merged = list(dict.fromkeys(existing + desired_vols)) # de-dup, preserve order
|
|
cfg['container']['valid_volumes'] = merged
|
|
|
|
# Write back with stable formatting. yaml.dump preserves enough
|
|
# structure for act_runner to parse; comments in the original config
|
|
# do get stripped — that's why we preserve the .pre-provision backup.
|
|
with open(cfg_path + '.tmp', 'w') as f:
|
|
yaml.safe_dump(cfg, f, default_flow_style=False, sort_keys=False)
|
|
os.replace(cfg_path + '.tmp', cfg_path)
|
|
print(f" container.force_pull : false")
|
|
print(f" container.options : {container_options}")
|
|
print(f" container.valid_volumes: {merged}")
|
|
PY
|
|
|
|
# ---------- 3. pre-pull the runner image -------------------------------
|
|
|
|
log "pulling $RUNNER_IMAGE (one-time; cron refreshes it nightly)"
|
|
docker pull "$RUNNER_IMAGE"
|
|
|
|
# ---------- 4. pre-clone the actions list ------------------------------
|
|
#
|
|
# act_runner expects clones at $cache/<sha256(url)> with the ref already
|
|
# checked out. We clone the default branch then fetch + check out the
|
|
# requested ref. Re-running fetches updates rather than re-cloning.
|
|
|
|
log "pre-cloning actions into $CACHE_BASE/act-actions"
|
|
for spec in "${ACTIONS[@]}"; do
|
|
if [[ "$spec" != *@* ]]; then
|
|
warn " skip '$spec' — must be owner/repo@ref"
|
|
continue
|
|
fi
|
|
repo="${spec%@*}"
|
|
ref="${spec##*@}"
|
|
url="https://github.com/${repo}"
|
|
dir="${CACHE_BASE}/act-actions/$(sha256_url "$url")"
|
|
|
|
if [[ -d "$dir/.git" ]]; then
|
|
log " refresh $repo @ $ref"
|
|
git -C "$dir" fetch --quiet --tags --prune origin
|
|
else
|
|
log " clone $repo @ $ref → $dir"
|
|
git clone --quiet "$url" "$dir"
|
|
fi
|
|
# Detach onto the requested ref. Works for branches, tags, and SHAs.
|
|
if ! git -C "$dir" -c advice.detachedHead=false checkout --quiet "$ref" 2>/dev/null; then
|
|
# If `ref` is a remote branch we haven't tracked yet, try origin/<ref>.
|
|
git -C "$dir" -c advice.detachedHead=false checkout --quiet "origin/$ref"
|
|
fi
|
|
done
|
|
|
|
# ---------- 5. golangci-lint -------------------------------------------
|
|
#
|
|
# Install the latest v2.x at /usr/local/bin/golangci-lint. Workflows
|
|
# that pin a specific version via the action's `version:` arg will
|
|
# still re-download — but jobs that don't pin (or pin to "latest"/"v2")
|
|
# get the host-installed binary for free.
|
|
|
|
log "installing/updating golangci-lint (latest v2.x) → /usr/local/bin"
|
|
GOLANGCI_INSTALL_URL="https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh"
|
|
# `-b` = install dir, `-d` = quiet "downloading" lines, no version arg
|
|
# means "latest" — which install.sh resolves to the latest v2 release
|
|
# from GitHub releases.
|
|
curl -fsSL "$GOLANGCI_INSTALL_URL" | sh -s -- -b /usr/local/bin >/dev/null
|
|
/usr/local/bin/golangci-lint --version || warn "golangci-lint install verification failed"
|
|
|
|
# ---------- 6. nightly refresh cron ------------------------------------
|
|
#
|
|
# Re-pulls the runner image, refreshes the action clones, and updates
|
|
# golangci-lint. Runs at 03:17 to dodge top-of-hour CI bursts.
|
|
|
|
CRON_PATH=/etc/cron.d/gitea-runner-refresh
|
|
REFRESH_SCRIPT=/usr/local/sbin/gitea-runner-refresh
|
|
|
|
log "writing $REFRESH_SCRIPT and $CRON_PATH"
|
|
|
|
# Materialise the actions list into the script so the cron is
|
|
# self-contained and surviving an edit to this file.
|
|
ACTIONS_LITERAL=""
|
|
for s in "${ACTIONS[@]}"; do
|
|
ACTIONS_LITERAL="${ACTIONS_LITERAL} \"$s\"\n"
|
|
done
|
|
|
|
cat >"$REFRESH_SCRIPT" <<EOF
|
|
#!/usr/bin/env bash
|
|
# Auto-generated by provision-gitea-runner.sh. Re-running the
|
|
# provisioning script regenerates this file.
|
|
set -euo pipefail
|
|
CACHE_BASE="$CACHE_BASE"
|
|
RUNNER_IMAGE="$RUNNER_IMAGE"
|
|
ACTIONS=(
|
|
$(printf ' "%s"\n' "${ACTIONS[@]}")
|
|
)
|
|
|
|
sha256_url() { printf '%s' "\$1" | sha256sum | awk '{print \$1}'; }
|
|
|
|
# 1. Refresh the runner-images base.
|
|
docker pull -q "\$RUNNER_IMAGE" >/dev/null
|
|
|
|
# 2. Refresh action clones.
|
|
for spec in "\${ACTIONS[@]}"; do
|
|
[[ "\$spec" == *@* ]] || continue
|
|
repo="\${spec%@*}"; ref="\${spec##*@}"
|
|
url="https://github.com/\$repo"
|
|
dir="\$CACHE_BASE/act-actions/\$(sha256_url "\$url")"
|
|
if [[ -d "\$dir/.git" ]]; then
|
|
git -C "\$dir" fetch --quiet --tags --prune origin || true
|
|
git -C "\$dir" -c advice.detachedHead=false checkout --quiet "\$ref" 2>/dev/null \\
|
|
|| git -C "\$dir" -c advice.detachedHead=false checkout --quiet "origin/\$ref" || true
|
|
fi
|
|
done
|
|
|
|
# 3. Refresh golangci-lint (latest v2.x). Tolerate transient
|
|
# GitHub-rate-limit failures — next night will retry.
|
|
curl -fsSL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh \\
|
|
| sh -s -- -b /usr/local/bin >/dev/null 2>&1 || true
|
|
EOF
|
|
chmod 0755 "$REFRESH_SCRIPT"
|
|
|
|
cat >"$CRON_PATH" <<EOF
|
|
# Auto-generated by provision-gitea-runner.sh. Refreshes the runner
|
|
# image, action clones, and golangci-lint every night at 03:17.
|
|
SHELL=/bin/bash
|
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
17 3 * * * root $REFRESH_SCRIPT >> /var/log/gitea-runner-refresh.log 2>&1
|
|
EOF
|
|
chmod 0644 "$CRON_PATH"
|
|
|
|
# ---------- 7. restart act_runner --------------------------------------
|
|
|
|
log "restarting act_runner.service to pick up the new config"
|
|
systemctl restart act_runner.service
|
|
sleep 2
|
|
systemctl is-active --quiet act_runner.service \
|
|
|| die "act_runner did not come back up — check 'journalctl -u act_runner -n 50'"
|
|
|
|
# ---------- 8. container-create benchmark ------------------------------
|
|
#
|
|
# Reports cold + warm `docker run --rm <image> true` time. Sanity check
|
|
# that overlay setup is fast on this host. Numbers > ~5s indicate a
|
|
# slow filesystem or DNS issue worth investigating separately.
|
|
|
|
log "benchmark: docker run --rm $RUNNER_IMAGE true"
|
|
{
|
|
printf ' cold (post-pull) : '
|
|
/usr/bin/time -f '%e s' docker run --rm "$RUNNER_IMAGE" true 2>&1 | tail -1
|
|
printf ' warm (immediate) : '
|
|
/usr/bin/time -f '%e s' docker run --rm "$RUNNER_IMAGE" true 2>&1 | tail -1
|
|
} || warn "benchmark failed — non-fatal"
|
|
|
|
# ---------- done -------------------------------------------------------
|
|
|
|
cat <<EOF
|
|
|
|
\033[1;32m==> Provisioning complete\033[0m
|
|
|
|
What changed on this host:
|
|
* /etc/act_runner/config.yaml — force_pull off, container.options +
|
|
valid_volumes set for the cache mounts. Pristine copy preserved
|
|
at ${ACT_RUNNER_CONFIG}.pre-provision.
|
|
* $CACHE_BASE/{go-mod,go-build,act-actions} — persistent caches.
|
|
* /usr/local/bin/golangci-lint — latest v2.x.
|
|
* $REFRESH_SCRIPT and $CRON_PATH — nightly refresh @ 03:17.
|
|
* Runner image pre-pulled.
|
|
|
|
\033[1;33mNote on Go cache + setup-go:\033[0m if your workflow uses
|
|
\`actions/setup-go\` with \`cache: true\`, the action will still tar/untar
|
|
the cache via the Gitea cache backend on every job — partially
|
|
defeating the persistent volume. For full speed-up, drop \`cache: true\`
|
|
from the workflow once the persistent volume is warm. Per-project
|
|
decision; this script doesn't touch workflows.
|
|
|
|
EOF
|