#!/usr/bin/env bash # actions-local.sh # Local GitHub Actions self-hosted runner lifecycle helper. set -euo pipefail RUNNER_DIR="${RUNNER_DIR:-$HOME/.periodvault-actions-runner}" RUNNER_LABELS="${RUNNER_LABELS:-periodvault}" RUNNER_NAME="${RUNNER_NAME:-$(hostname)-periodvault}" RUNNER_WORKDIR="${RUNNER_WORKDIR:-_work}" RUNNER_PID_FILE="${RUNNER_PID_FILE:-$RUNNER_DIR/.runner.pid}" RUNNER_LOG_FILE="${RUNNER_LOG_FILE:-$RUNNER_DIR/runner.log}" if git config --get remote.origin.url >/dev/null 2>&1; then ORIGIN_URL="$(git config --get remote.origin.url)" else ORIGIN_URL="" fi if [[ -n "$ORIGIN_URL" && "$ORIGIN_URL" =~ ^git@github\.com:(.*)\.git$ ]]; then RUNNER_URL_DEFAULT="https://github.com/${BASH_REMATCH[1]}" elif [[ -n "$ORIGIN_URL" && "$ORIGIN_URL" =~ ^https://github\.com/.*$ ]]; then RUNNER_URL_DEFAULT="${ORIGIN_URL%.git}" else RUNNER_URL_DEFAULT="" fi RUNNER_URL="${RUNNER_URL:-$RUNNER_URL_DEFAULT}" ACT_WORKFLOW="${ACT_WORKFLOW:-.github/workflows/ci.yml}" ACT_IMAGE="${ACT_IMAGE:-ghcr.io/catthehacker/ubuntu:act-latest}" ACT_DOCKER_SOCKET="${ACT_DOCKER_SOCKET:-/Users/s/.colima/augur-actions/docker.sock}" ACT_DAEMON_SOCKET="${ACT_DAEMON_SOCKET:-/var/run/docker.sock}" ACT_DOCKER_CONFIG="${ACT_DOCKER_CONFIG:-/tmp/act-docker-config}" usage() { cat < [job-id] Environment variables: RUNNER_DIR Runner installation directory (default: $RUNNER_DIR) RUNNER_URL GitHub repo/org URL for runner registration RUNNER_TOKEN Registration/removal token (required for setup/remove) RUNNER_LABELS Runner labels (default: $RUNNER_LABELS) RUNNER_NAME Runner name (default: $RUNNER_NAME) RUNNER_WORKDIR Runner work dir (default: $RUNNER_WORKDIR) Local Actions execution (`run`) variables: ACT_WORKFLOW Workflow file path (default: $ACT_WORKFLOW) ACT_IMAGE Container image for self-hosted label mapping (default: $ACT_IMAGE) ACT_DOCKER_SOCKET Docker host socket (default: $ACT_DOCKER_SOCKET) ACT_DAEMON_SOCKET In-container daemon socket path (default: $ACT_DAEMON_SOCKET) ACT_DOCKER_CONFIG Docker config dir used by act (default: $ACT_DOCKER_CONFIG) EOF } ensure_runner_binaries() { if [[ ! -x "$RUNNER_DIR/config.sh" || ! -x "$RUNNER_DIR/run.sh" ]]; then echo "[actions-local] Missing runner binaries in $RUNNER_DIR." echo "[actions-local] Download and extract GitHub runner there first." exit 1 fi } ensure_runner_url() { if [[ -z "$RUNNER_URL" ]]; then echo "[actions-local] RUNNER_URL is empty." echo "[actions-local] Set RUNNER_URL=https://github.com// and retry." exit 1 fi } require_token() { if [[ -z "${RUNNER_TOKEN:-}" ]]; then echo "[actions-local] RUNNER_TOKEN is required for this command." exit 1 fi } cmd_setup() { ensure_runner_binaries ensure_runner_url require_token if [[ -f "$RUNNER_DIR/.runner" ]]; then echo "[actions-local] Runner already configured in $RUNNER_DIR (idempotent no-op)." exit 0 fi ( cd "$RUNNER_DIR" ./config.sh \ --unattended \ --replace \ --url "$RUNNER_URL" \ --token "$RUNNER_TOKEN" \ --name "$RUNNER_NAME" \ --labels "$RUNNER_LABELS" \ --work "$RUNNER_WORKDIR" ) echo "[actions-local] Runner configured." } cmd_start() { ensure_runner_binaries if [[ ! -f "$RUNNER_DIR/.runner" ]]; then echo "[actions-local] Runner not configured. Run setup first." exit 1 fi if [[ -f "$RUNNER_PID_FILE" ]] && kill -0 "$(cat "$RUNNER_PID_FILE")" >/dev/null 2>&1; then echo "[actions-local] Runner already running (pid $(cat "$RUNNER_PID_FILE"))." exit 0 fi ( cd "$RUNNER_DIR" nohup ./run.sh >"$RUNNER_LOG_FILE" 2>&1 & echo $! >"$RUNNER_PID_FILE" ) echo "[actions-local] Runner started (pid $(cat "$RUNNER_PID_FILE"))." echo "[actions-local] Log: $RUNNER_LOG_FILE" } cmd_stop() { if [[ ! -f "$RUNNER_PID_FILE" ]]; then echo "[actions-local] Runner is not running." exit 0 fi pid="$(cat "$RUNNER_PID_FILE")" if kill -0 "$pid" >/dev/null 2>&1; then kill "$pid" rm -f "$RUNNER_PID_FILE" echo "[actions-local] Runner stopped (pid $pid)." else rm -f "$RUNNER_PID_FILE" echo "[actions-local] Runner pid file was stale; cleaned up." fi } cmd_status() { if [[ -f "$RUNNER_DIR/.runner" ]]; then echo "[actions-local] configured: yes" else echo "[actions-local] configured: no" fi if [[ -f "$RUNNER_PID_FILE" ]] && kill -0 "$(cat "$RUNNER_PID_FILE")" >/dev/null 2>&1; then echo "[actions-local] running: yes (pid $(cat "$RUNNER_PID_FILE"))" else echo "[actions-local] running: no" fi echo "[actions-local] runner-dir: $RUNNER_DIR" echo "[actions-local] runner-labels: $RUNNER_LABELS" } cmd_remove() { ensure_runner_binaries require_token if [[ ! -f "$RUNNER_DIR/.runner" ]]; then echo "[actions-local] Runner is not configured." exit 0 fi ( cd "$RUNNER_DIR" ./config.sh remove --token "$RUNNER_TOKEN" ) echo "[actions-local] Runner registration removed." } cmd_run() { local job="${1:-sdd-gate}" if ! command -v act >/dev/null 2>&1; then echo "[actions-local] 'act' is required for local workflow execution." exit 1 fi mkdir -p "$ACT_DOCKER_CONFIG" if [[ ! -f "$ACT_DOCKER_CONFIG/config.json" ]]; then printf '{"auths":{}}\n' >"$ACT_DOCKER_CONFIG/config.json" fi DOCKER_CONFIG="$ACT_DOCKER_CONFIG" \ DOCKER_HOST="unix://$ACT_DOCKER_SOCKET" \ act -W "$ACT_WORKFLOW" \ -j "$job" \ -P "self-hosted=$ACT_IMAGE" \ -P "macos-latest=$ACT_IMAGE" \ --container-architecture linux/amd64 \ --container-daemon-socket "$ACT_DAEMON_SOCKET" } COMMAND="${1:-}" case "$COMMAND" in setup) cmd_setup ;; start) cmd_start ;; stop) cmd_stop ;; status) cmd_status ;; remove) cmd_remove ;; run) cmd_run "${2:-}" ;; ""|--help|-h) usage ;; *) echo "[actions-local] Unknown command: $COMMAND" usage exit 1 ;; esac