Files
infrastructure/servers/gcp/runners/startup.sh
T
Will Anderson 0c32964ead ci: add gitea actions runner on GCP with WIF-backed deploy SA
A single e2-standard-4 GCE VM in us-central1-a runs act_runner,
registered to git.neuralplatform.ai. Workload Identity Federation
binds the runner to the existing neuron-ci-pusher SA (artifactregistry.writer
+ run.developer + serviceAccountUser on the runtime SAs). No long-lived
JSON keys live on the runner; the GCP_SA_KEY secret remains as a fallback
in case the Gitea OIDC issuer isn't reachable from oauth2.googleapis.com.

The runner VM has its own minimal-scope SA (gitea-runner-vm) that can
only read the registration token from Secret Manager — splitting boot
identity from deploy identity so a runner compromise doesn't grant push.

SSH is IAP-only (no public ingress on :22). Reference workflow lives at
products/web/.gitea/workflows/deploy.yaml and takes manual gcloud out of
the loop: push to main triggers build, push to AR, parallel deploy to
all 3 marketing prod regions, traffic flip, smoke check.
2026-05-02 12:45:25 -05:00

98 lines
3.4 KiB
Bash

#!/usr/bin/env bash
# servers/gcp/runners/startup.sh
#
# Boot script for the Gitea Actions runner GCE VM.
# - Installs docker + git + jq + the Forgejo/Gitea act_runner binary.
# - Pulls the runner registration token from GCP Secret Manager
# (the VM's attached service account has secretmanager.secretAccessor on it).
# - Registers against https://git.neuralplatform.ai with labels matching
# the workflows that target ubuntu-latest hosts.
# - Starts act_runner under systemd so it auto-recovers on reboot.
#
# Logs land at /var/log/runner-bootstrap.log for debugging from IAP SSH.
set -euxo pipefail
exec > >(tee /var/log/runner-bootstrap.log) 2>&1
apt-get update
apt-get install -y curl ca-certificates docker.io git jq
# Make docker usable by the unprivileged runner user
systemctl enable --now docker
useradd -m -s /bin/bash runner || true
usermod -aG docker runner
# act_runner — pinned to a known-good release. Bump RUNNER_VERSION when
# upgrading. The project moved from gitea/act_runner to gitea/runner around
# the 0.6.x series; the binary inside the asset is still called act_runner.
# Latest releases at: https://gitea.com/gitea/runner/releases
RUNNER_VERSION="0.6.1"
curl -fsSL \
"https://gitea.com/gitea/runner/releases/download/v${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-amd64" \
-o /usr/local/bin/act_runner
chmod +x /usr/local/bin/act_runner
mkdir -p /opt/runner
chown -R runner:runner /opt/runner
# Pull registration token. Fail fast if the secret is missing — the runner
# is useless without it, and a silent fallback would leave us debugging an
# unregistered VM later.
GITEA_RUNNER_TOKEN="$(gcloud secrets versions access latest \
--secret=gitea-runner-token \
--project=neuron-785695)"
# Generate a baseline config so the daemon has a sane starting point. The
# register step writes the .runner state file into /opt/runner.
sudo -u runner /usr/local/bin/act_runner generate-config > /opt/runner/config.yaml
chown runner:runner /opt/runner/config.yaml
cd /opt/runner
sudo -u runner /usr/local/bin/act_runner register \
--no-interactive \
--instance https://git.neuralplatform.ai \
--token "${GITEA_RUNNER_TOKEN}" \
--name "gcp-us-central1-runner-1" \
--labels "ubuntu-latest,ubuntu-22.04,ubuntu-24.04"
# Belt-and-braces: act_runner v0.6 has a habit of wrapping bare labels with
# the default `:docker://docker.gitea.com/runner-images:<label>` shape on
# first daemon start. We want host execution (build-stage.sh uses the
# host's Docker socket), so re-write the labels in the persisted state
# file before we start the daemon.
python3 - <<'PY'
import json
p = "/opt/runner/.runner"
with open(p) as f: d = json.load(f)
d["labels"] = ["ubuntu-latest", "ubuntu-22.04", "ubuntu-24.04"]
with open(p, "w") as f: json.dump(d, f, indent=2)
PY
chown runner:runner /opt/runner/.runner
cat > /etc/systemd/system/act_runner.service <<'EOF'
[Unit]
Description=Gitea Actions runner (act_runner)
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=runner
WorkingDirectory=/opt/runner
ExecStart=/usr/local/bin/act_runner daemon --config /opt/runner/config.yaml
Restart=always
RestartSec=5
# Pass through GCP metadata-server access so workflows can call gcloud
Environment=HOME=/home/runner
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now act_runner
echo "act_runner registered and running"