745 lines
22 KiB
Bash
Executable File
745 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# runner.sh — Setup, manage, and tear down a GitHub Actions self-hosted runner.
|
|
#
|
|
# Supports two platforms:
|
|
# - macOS: Installs the runner agent natively, manages it as a launchd service.
|
|
# - Linux: Delegates to Docker-based runner infrastructure in infra/runners/.
|
|
#
|
|
# Typical flow:
|
|
# 1) ./scripts/runner.sh --mode setup # install/configure runner
|
|
# 2) ./scripts/runner.sh --mode status # verify runner is online
|
|
# 3) (push/PR triggers CI on the self-hosted runner)
|
|
# 4) ./scripts/runner.sh --mode stop # stop runner
|
|
# 5) ./scripts/runner.sh --mode uninstall # deregister and clean up
|
|
|
|
set -euo pipefail
|
|
|
|
MODE=""
|
|
RUNNER_DIR="${AUGUR_RUNNER_DIR:-${HOME}/.augur-runner}"
|
|
RUNNER_LABELS="self-hosted,macOS,ARM64"
|
|
RUNNER_NAME=""
|
|
REPO_SLUG=""
|
|
REG_TOKEN=""
|
|
FORCE=false
|
|
FOREGROUND=false
|
|
PUSH_REGISTRY=""
|
|
|
|
PLIST_LABEL="com.augur.actions-runner"
|
|
PLIST_PATH="${HOME}/Library/LaunchAgents/${PLIST_LABEL}.plist"
|
|
|
|
# Resolved during Linux operations
|
|
INFRA_DIR=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
./scripts/runner.sh --mode <setup|start|stop|status|build-image|uninstall> [options]
|
|
|
|
Required:
|
|
--mode MODE One of: setup, start, stop, status, build-image, uninstall
|
|
|
|
Options (macOS):
|
|
--runner-dir DIR Installation directory (default: ~/.augur-runner)
|
|
--labels LABELS Comma-separated labels (default: self-hosted,macOS,ARM64)
|
|
--name NAME Runner name (default: augur-<hostname>)
|
|
--repo OWNER/REPO GitHub repository (default: auto-detected from git remote)
|
|
--token TOKEN Registration/removal token (prompted if not provided)
|
|
--force Force re-setup even if already configured
|
|
--foreground Start in foreground instead of launchd service
|
|
|
|
Options (Linux — Docker mode):
|
|
On Linux, this script delegates to Docker Compose in infra/runners/.
|
|
Configuration is managed via .env and envs/*.env files.
|
|
See infra/runners/README.md for details.
|
|
|
|
Options (build-image):
|
|
--push REGISTRY Tag and push to a registry (e.g. 192.168.1.82:5000)
|
|
|
|
Common:
|
|
-h, --help Show this help
|
|
|
|
Examples (macOS):
|
|
./scripts/runner.sh --mode setup
|
|
./scripts/runner.sh --mode setup --token ghp_xxxxx
|
|
./scripts/runner.sh --mode start
|
|
./scripts/runner.sh --mode start --foreground
|
|
./scripts/runner.sh --mode status
|
|
./scripts/runner.sh --mode stop
|
|
./scripts/runner.sh --mode uninstall
|
|
|
|
Examples (Linux):
|
|
./scripts/runner.sh --mode setup # prompts for .env, starts runner
|
|
./scripts/runner.sh --mode start # docker compose up -d
|
|
./scripts/runner.sh --mode stop # docker compose down
|
|
./scripts/runner.sh --mode status # docker compose ps + logs
|
|
./scripts/runner.sh --mode uninstall # docker compose down -v --rmi local
|
|
|
|
Examples (build-image — works on any OS):
|
|
./scripts/runner.sh --mode build-image # build locally
|
|
./scripts/runner.sh --mode build-image --push 192.168.1.82:5000 # build + push to registry
|
|
|
|
Environment overrides:
|
|
AUGUR_RUNNER_DIR Runner installation directory (macOS only)
|
|
EOF
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers (consistent with actions-local.sh)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log() {
|
|
printf '[runner] %s\n' "$*"
|
|
}
|
|
|
|
warn() {
|
|
printf '[runner] WARNING: %s\n' "$*" >&2
|
|
}
|
|
|
|
die() {
|
|
printf '[runner] ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_cmd() {
|
|
local cmd="$1"
|
|
command -v "$cmd" >/dev/null 2>&1 || die "required command not found: $cmd"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Platform detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
detect_os() {
|
|
case "$(uname -s)" in
|
|
Darwin) printf 'darwin' ;;
|
|
Linux) printf 'linux' ;;
|
|
*) die "Unsupported OS: $(uname -s). This script supports macOS and Linux." ;;
|
|
esac
|
|
}
|
|
|
|
ensure_macos() {
|
|
[[ "$(detect_os)" == "darwin" ]] || die "This operation requires macOS."
|
|
}
|
|
|
|
# Locate the infra/runners/ directory relative to the repo root.
|
|
# The script lives at scripts/runner.sh, so repo root is one level up.
|
|
find_infra_dir() {
|
|
local script_dir
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
local repo_root="${script_dir}/.."
|
|
INFRA_DIR="$(cd "${repo_root}/infra/runners" 2>/dev/null && pwd)" || true
|
|
|
|
if [[ -z "$INFRA_DIR" ]] || [[ ! -f "${INFRA_DIR}/docker-compose.yml" ]]; then
|
|
die "Could not find infra/runners/docker-compose.yml. Ensure you are running from the augur repo."
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Argument parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--mode)
|
|
shift; [[ $# -gt 0 ]] || die "--mode requires a value"
|
|
MODE="$1"; shift ;;
|
|
--runner-dir)
|
|
shift; [[ $# -gt 0 ]] || die "--runner-dir requires a value"
|
|
RUNNER_DIR="$1"; shift ;;
|
|
--labels)
|
|
shift; [[ $# -gt 0 ]] || die "--labels requires a value"
|
|
RUNNER_LABELS="$1"; shift ;;
|
|
--name)
|
|
shift; [[ $# -gt 0 ]] || die "--name requires a value"
|
|
RUNNER_NAME="$1"; shift ;;
|
|
--repo)
|
|
shift; [[ $# -gt 0 ]] || die "--repo requires a value"
|
|
REPO_SLUG="$1"; shift ;;
|
|
--token)
|
|
shift; [[ $# -gt 0 ]] || die "--token requires a value"
|
|
REG_TOKEN="$1"; shift ;;
|
|
--force)
|
|
FORCE=true; shift ;;
|
|
--foreground)
|
|
FOREGROUND=true; shift ;;
|
|
--push)
|
|
shift; [[ $# -gt 0 ]] || die "--push requires a registry address (e.g. 192.168.1.82:5000)"
|
|
PUSH_REGISTRY="$1"; shift ;;
|
|
-h|--help)
|
|
usage; exit 0 ;;
|
|
*)
|
|
die "unknown argument: $1" ;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$MODE" ]] || die "--mode is required (setup|start|stop|status|build-image|uninstall)"
|
|
case "$MODE" in
|
|
setup|start|stop|status|build-image|uninstall) ;;
|
|
*) die "invalid --mode: $MODE (expected setup|start|stop|status|build-image|uninstall)" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Repo detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
detect_repo() {
|
|
if [[ -n "$REPO_SLUG" ]]; then
|
|
return
|
|
fi
|
|
|
|
local remote_url=""
|
|
remote_url="$(git remote get-url origin 2>/dev/null || true)"
|
|
if [[ -z "$remote_url" ]]; then
|
|
die "Could not detect repository from git remote. Use --repo OWNER/REPO."
|
|
fi
|
|
|
|
# Extract OWNER/REPO from HTTPS or SSH URLs
|
|
REPO_SLUG="$(printf '%s' "$remote_url" \
|
|
| sed -E 's#^(https?://github\.com/|git@github\.com:)##' \
|
|
| sed -E 's/\.git$//')"
|
|
|
|
if [[ -z "$REPO_SLUG" ]] || ! printf '%s' "$REPO_SLUG" | grep -qE '^[^/]+/[^/]+$'; then
|
|
die "Could not parse OWNER/REPO from remote URL: $remote_url. Use --repo OWNER/REPO."
|
|
fi
|
|
|
|
log "Auto-detected repository: $REPO_SLUG"
|
|
}
|
|
|
|
# ===========================================================================
|
|
# macOS: Native runner agent + launchd service
|
|
# ===========================================================================
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Runner download and verification (macOS)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
detect_arch() {
|
|
local arch
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
arm64|aarch64) printf 'arm64' ;;
|
|
x86_64) printf 'x64' ;;
|
|
*) die "Unsupported architecture: $arch" ;;
|
|
esac
|
|
}
|
|
|
|
download_runner() {
|
|
require_cmd curl
|
|
require_cmd shasum
|
|
require_cmd tar
|
|
|
|
local arch
|
|
arch="$(detect_arch)"
|
|
|
|
log "Fetching latest runner release metadata..."
|
|
local release_json
|
|
release_json="$(curl -fsSL "https://api.github.com/repos/actions/runner/releases/latest")"
|
|
|
|
local version
|
|
version="$(printf '%s' "$release_json" | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/')"
|
|
if [[ -z "$version" ]]; then
|
|
die "Could not determine latest runner version from GitHub API."
|
|
fi
|
|
log "Latest runner version: $version"
|
|
|
|
local tarball="actions-runner-osx-${arch}-${version}.tar.gz"
|
|
local download_url="https://github.com/actions/runner/releases/download/v${version}/${tarball}"
|
|
|
|
# Extract expected SHA256 from release body.
|
|
# The body contains HTML comments like:
|
|
# <!-- BEGIN SHA osx-arm64 -->HASH<!-- END SHA osx-arm64 -->
|
|
local sha_marker="osx-${arch}"
|
|
local expected_sha=""
|
|
expected_sha="$(printf '%s' "$release_json" \
|
|
| python3 -c "
|
|
import json,sys,re
|
|
body = json.load(sys.stdin).get('body','')
|
|
m = re.search(r'<!-- BEGIN SHA ${sha_marker} -->([0-9a-f]{64})<!-- END SHA ${sha_marker} -->', body)
|
|
print(m.group(1) if m else '')
|
|
" 2>/dev/null || true)"
|
|
|
|
mkdir -p "$RUNNER_DIR"
|
|
local dest="${RUNNER_DIR}/${tarball}"
|
|
|
|
if [[ -f "$dest" ]]; then
|
|
log "Tarball already exists: $dest"
|
|
else
|
|
log "Downloading: $download_url"
|
|
curl -fSL -o "$dest" "$download_url"
|
|
fi
|
|
|
|
if [[ -n "$expected_sha" ]]; then
|
|
log "Verifying SHA256 checksum..."
|
|
local actual_sha
|
|
actual_sha="$(shasum -a 256 "$dest" | awk '{print $1}')"
|
|
if [[ "$actual_sha" != "$expected_sha" ]]; then
|
|
rm -f "$dest"
|
|
die "Checksum mismatch. Expected: $expected_sha, Got: $actual_sha"
|
|
fi
|
|
log "Checksum verified."
|
|
else
|
|
warn "Could not extract expected SHA256 from release metadata; skipping verification."
|
|
fi
|
|
|
|
log "Extracting runner into $RUNNER_DIR..."
|
|
tar -xzf "$dest" -C "$RUNNER_DIR"
|
|
rm -f "$dest"
|
|
|
|
log "Runner extracted (version $version)."
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registration (macOS)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
prompt_token() {
|
|
if [[ -n "$REG_TOKEN" ]]; then
|
|
return
|
|
fi
|
|
|
|
log ""
|
|
log "A registration token is required."
|
|
log "Obtain one from: https://github.com/${REPO_SLUG}/settings/actions/runners/new"
|
|
log "Or via the API:"
|
|
log " curl -X POST -H 'Authorization: token YOUR_PAT' \\"
|
|
log " https://api.github.com/repos/${REPO_SLUG}/actions/runners/registration-token"
|
|
log ""
|
|
printf '[runner] Enter registration token: '
|
|
read -r REG_TOKEN
|
|
[[ -n "$REG_TOKEN" ]] || die "No token provided."
|
|
}
|
|
|
|
register_runner() {
|
|
if [[ -z "$RUNNER_NAME" ]]; then
|
|
RUNNER_NAME="augur-$(hostname -s)"
|
|
fi
|
|
|
|
log "Registering runner '${RUNNER_NAME}' with labels '${RUNNER_LABELS}'..."
|
|
|
|
local config_args=(
|
|
--url "https://github.com/${REPO_SLUG}"
|
|
--token "$REG_TOKEN"
|
|
--name "$RUNNER_NAME"
|
|
--labels "$RUNNER_LABELS"
|
|
--work "${RUNNER_DIR}/_work"
|
|
--unattended
|
|
)
|
|
|
|
if [[ "$FORCE" == "true" ]]; then
|
|
config_args+=(--replace)
|
|
fi
|
|
|
|
"${RUNNER_DIR}/config.sh" "${config_args[@]}"
|
|
log "Runner registered."
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# launchd service management (macOS)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
create_plist() {
|
|
mkdir -p "${RUNNER_DIR}/logs"
|
|
mkdir -p "$(dirname "$PLIST_PATH")"
|
|
|
|
cat > "$PLIST_PATH" <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>${PLIST_LABEL}</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>${RUNNER_DIR}/run.sh</string>
|
|
</array>
|
|
<key>WorkingDirectory</key>
|
|
<string>${RUNNER_DIR}</string>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>StandardOutPath</key>
|
|
<string>${RUNNER_DIR}/logs/stdout.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>${RUNNER_DIR}/logs/stderr.log</string>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>PATH</key>
|
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
<key>HOME</key>
|
|
<string>${HOME}</string>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
log "Launchd plist created: $PLIST_PATH"
|
|
}
|
|
|
|
load_service() {
|
|
if launchctl list 2>/dev/null | grep -q "$PLIST_LABEL"; then
|
|
log "Service already loaded; unloading first..."
|
|
launchctl unload "$PLIST_PATH" 2>/dev/null || true
|
|
fi
|
|
|
|
launchctl load "$PLIST_PATH"
|
|
log "Service loaded."
|
|
}
|
|
|
|
unload_service() {
|
|
if launchctl list 2>/dev/null | grep -q "$PLIST_LABEL"; then
|
|
launchctl unload "$PLIST_PATH" 2>/dev/null || true
|
|
log "Service unloaded."
|
|
else
|
|
log "Service is not loaded."
|
|
fi
|
|
}
|
|
|
|
service_is_running() {
|
|
launchctl list 2>/dev/null | grep -q "$PLIST_LABEL"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# macOS mode implementations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
do_setup_darwin() {
|
|
detect_repo
|
|
|
|
if [[ -f "${RUNNER_DIR}/.runner" ]] && [[ "$FORCE" != "true" ]]; then
|
|
log "Runner already configured at $RUNNER_DIR."
|
|
log "Use --force to re-setup."
|
|
do_status_darwin
|
|
return
|
|
fi
|
|
|
|
download_runner
|
|
prompt_token
|
|
register_runner
|
|
create_plist
|
|
load_service
|
|
|
|
log ""
|
|
log "Setup complete. Runner is registered and running."
|
|
log ""
|
|
log "To activate self-hosted CI, set the repository variable CI_RUNS_ON to:"
|
|
log ' ["self-hosted", "macOS", "ARM64"]'
|
|
log "in Settings > Secrets and variables > Actions > Variables."
|
|
log ""
|
|
log "Or via CLI:"
|
|
log " gh variable set CI_RUNS_ON --body '[\"self-hosted\", \"macOS\", \"ARM64\"]'"
|
|
log ""
|
|
log "Energy saver: ensure your Mac does not sleep while the runner is active."
|
|
log " System Settings > Energy Saver > Prevent automatic sleeping"
|
|
}
|
|
|
|
do_start_darwin() {
|
|
[[ -f "${RUNNER_DIR}/.runner" ]] || die "Runner not configured. Run --mode setup first."
|
|
|
|
if [[ "$FOREGROUND" == "true" ]]; then
|
|
log "Starting runner in foreground (Ctrl-C to stop)..."
|
|
exec "${RUNNER_DIR}/run.sh"
|
|
fi
|
|
|
|
if service_is_running; then
|
|
log "Runner service is already running."
|
|
return
|
|
fi
|
|
|
|
if [[ ! -f "$PLIST_PATH" ]]; then
|
|
log "Plist not found; recreating..."
|
|
create_plist
|
|
fi
|
|
|
|
load_service
|
|
log "Runner started."
|
|
}
|
|
|
|
do_stop_darwin() {
|
|
unload_service
|
|
log "Runner stopped."
|
|
}
|
|
|
|
do_status_darwin() {
|
|
log "Runner directory: $RUNNER_DIR"
|
|
|
|
if [[ ! -f "${RUNNER_DIR}/.runner" ]]; then
|
|
log "Status: NOT CONFIGURED"
|
|
log "Run --mode setup to install and register the runner."
|
|
return
|
|
fi
|
|
|
|
# Parse runner config
|
|
local runner_name=""
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
runner_name="$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('agentName',''))" "${RUNNER_DIR}/.runner" 2>/dev/null || true)"
|
|
fi
|
|
if [[ -z "$runner_name" ]]; then
|
|
runner_name="(could not parse)"
|
|
fi
|
|
|
|
log "Runner name: $runner_name"
|
|
|
|
if service_is_running; then
|
|
log "Service: RUNNING"
|
|
else
|
|
log "Service: STOPPED"
|
|
fi
|
|
|
|
if pgrep -f "Runner.Listener" >/dev/null 2>&1; then
|
|
log "Process: ACTIVE (Runner.Listener found)"
|
|
else
|
|
log "Process: INACTIVE"
|
|
fi
|
|
|
|
# Show recent logs
|
|
local log_file="${RUNNER_DIR}/logs/stdout.log"
|
|
if [[ -f "$log_file" ]]; then
|
|
log ""
|
|
log "Recent log output (last 10 lines):"
|
|
tail -n 10 "$log_file" 2>/dev/null || true
|
|
fi
|
|
|
|
local diag_dir="${RUNNER_DIR}/_diag"
|
|
if [[ -d "$diag_dir" ]]; then
|
|
local latest_diag
|
|
latest_diag="$(ls -t "${diag_dir}"/Runner_*.log 2>/dev/null | head -n1 || true)"
|
|
if [[ -n "$latest_diag" ]]; then
|
|
log ""
|
|
log "Latest runner diagnostic (last 5 lines):"
|
|
tail -n 5 "$latest_diag" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
do_uninstall_darwin() {
|
|
log "Uninstalling self-hosted runner..."
|
|
|
|
# Stop service first
|
|
unload_service
|
|
|
|
# Remove plist
|
|
if [[ -f "$PLIST_PATH" ]]; then
|
|
rm -f "$PLIST_PATH"
|
|
log "Removed plist: $PLIST_PATH"
|
|
fi
|
|
|
|
# Deregister from GitHub
|
|
if [[ -f "${RUNNER_DIR}/config.sh" ]]; then
|
|
if [[ -z "$REG_TOKEN" ]]; then
|
|
detect_repo
|
|
log ""
|
|
log "A removal token is required to deregister the runner."
|
|
log "Obtain one from: https://github.com/${REPO_SLUG}/settings/actions/runners"
|
|
log "Or via the API:"
|
|
log " curl -X POST -H 'Authorization: token YOUR_PAT' \\"
|
|
log " https://api.github.com/repos/${REPO_SLUG}/actions/runners/remove-token"
|
|
log ""
|
|
printf '[runner] Enter removal token (or press Enter to skip deregistration): '
|
|
read -r REG_TOKEN
|
|
fi
|
|
|
|
if [[ -n "$REG_TOKEN" ]]; then
|
|
"${RUNNER_DIR}/config.sh" remove --token "$REG_TOKEN" || warn "Deregistration failed; you may need to remove the runner manually from GitHub settings."
|
|
log "Runner deregistered from GitHub."
|
|
else
|
|
warn "Skipping deregistration. Remove the runner manually from GitHub settings."
|
|
fi
|
|
fi
|
|
|
|
# Clean up runner directory
|
|
if [[ -d "$RUNNER_DIR" ]]; then
|
|
log "Removing runner directory: $RUNNER_DIR"
|
|
rm -rf "$RUNNER_DIR"
|
|
log "Runner directory removed."
|
|
fi
|
|
|
|
log "Uninstall complete."
|
|
}
|
|
|
|
# ===========================================================================
|
|
# Linux: Docker-based runner via infra/runners/
|
|
# ===========================================================================
|
|
|
|
# Ensure Docker and docker compose are available.
|
|
ensure_docker() {
|
|
require_cmd docker
|
|
|
|
# Check for docker compose (v2 plugin or standalone)
|
|
if docker compose version >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
|
|
if command -v docker-compose >/dev/null 2>&1; then
|
|
warn "Found docker-compose (standalone). docker compose v2 plugin is recommended."
|
|
return
|
|
fi
|
|
|
|
die "docker compose is required. Install Docker Compose v2: https://docs.docker.com/compose/install/"
|
|
}
|
|
|
|
# Run docker compose in the infra/runners directory.
|
|
# Accepts any docker compose subcommand and arguments.
|
|
compose() {
|
|
docker compose -f "${INFRA_DIR}/docker-compose.yml" "$@"
|
|
}
|
|
|
|
do_build_image() {
|
|
find_infra_dir
|
|
ensure_docker
|
|
|
|
local dockerfile_dir="${INFRA_DIR}"
|
|
|
|
# Determine the image tag based on whether --push was given.
|
|
# With --push: tag includes the registry so docker push knows where to send it.
|
|
# Without --push: clean local name.
|
|
local image_tag="augur-runner:latest"
|
|
if [[ -n "$PUSH_REGISTRY" ]]; then
|
|
image_tag="${PUSH_REGISTRY}/augur-runner:latest"
|
|
fi
|
|
|
|
# Always target linux/amd64 — the Dockerfile hardcodes x86_64 binaries
|
|
# (Go linux-amd64, runner agent linux-x64). This ensures correct arch
|
|
# even when building on an ARM Mac.
|
|
log "Building runner image: ${image_tag} (platform: linux/amd64)"
|
|
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 --pull -t "$image_tag" "$dockerfile_dir"
|
|
|
|
if [[ -n "$PUSH_REGISTRY" ]]; then
|
|
log "Pushing to ${PUSH_REGISTRY}..."
|
|
docker push "$image_tag"
|
|
log "Image pushed to ${image_tag}"
|
|
else
|
|
log "Image built locally as ${image_tag}"
|
|
log "Use --push <registry> to push to a remote registry."
|
|
fi
|
|
}
|
|
|
|
do_setup_linux() {
|
|
find_infra_dir
|
|
ensure_docker
|
|
|
|
log "Docker-based runner setup (infra/runners/)"
|
|
log ""
|
|
|
|
# Create .env from template if it doesn't exist
|
|
if [[ ! -f "${INFRA_DIR}/.env" ]]; then
|
|
if [[ -f "${INFRA_DIR}/.env.example" ]]; then
|
|
cp "${INFRA_DIR}/.env.example" "${INFRA_DIR}/.env"
|
|
log "Created ${INFRA_DIR}/.env from template."
|
|
log "Edit this file to set your GITHUB_PAT."
|
|
log ""
|
|
printf '[runner] Enter your GitHub PAT (or press Enter to edit .env manually later): '
|
|
read -r pat_input
|
|
if [[ -n "$pat_input" ]]; then
|
|
sed -i "s/^GITHUB_PAT=.*/GITHUB_PAT=${pat_input}/" "${INFRA_DIR}/.env"
|
|
log "GITHUB_PAT set in .env"
|
|
fi
|
|
else
|
|
die "Missing .env.example template in ${INFRA_DIR}"
|
|
fi
|
|
else
|
|
log ".env already exists; skipping."
|
|
fi
|
|
|
|
# Create per-repo env from template if it doesn't exist
|
|
if [[ ! -f "${INFRA_DIR}/envs/augur.env" ]]; then
|
|
if [[ -f "${INFRA_DIR}/envs/augur.env.example" ]]; then
|
|
cp "${INFRA_DIR}/envs/augur.env.example" "${INFRA_DIR}/envs/augur.env"
|
|
log "Created ${INFRA_DIR}/envs/augur.env from template."
|
|
log "Edit this file to configure REPO_URL, RUNNER_NAME, and resource limits."
|
|
else
|
|
die "Missing envs/augur.env.example template in ${INFRA_DIR}"
|
|
fi
|
|
else
|
|
log "envs/augur.env already exists; skipping."
|
|
fi
|
|
|
|
log ""
|
|
log "Starting runner..."
|
|
compose up -d
|
|
|
|
log ""
|
|
log "Setup complete. Verify with: ./scripts/runner.sh --mode status"
|
|
log ""
|
|
log "To activate self-hosted CI, set the repository variable CI_RUNS_ON to:"
|
|
log ' ["self-hosted", "Linux", "X64"]'
|
|
log ""
|
|
log "Via CLI:"
|
|
log " gh variable set CI_RUNS_ON --body '[\"self-hosted\", \"Linux\", \"X64\"]'"
|
|
}
|
|
|
|
do_start_linux() {
|
|
find_infra_dir
|
|
ensure_docker
|
|
|
|
log "Starting Docker runner..."
|
|
compose up -d
|
|
log "Runner started."
|
|
}
|
|
|
|
do_stop_linux() {
|
|
find_infra_dir
|
|
ensure_docker
|
|
|
|
log "Stopping Docker runner..."
|
|
compose down
|
|
log "Runner stopped."
|
|
}
|
|
|
|
do_status_linux() {
|
|
find_infra_dir
|
|
ensure_docker
|
|
|
|
log "Docker runner status (infra/runners/):"
|
|
log ""
|
|
compose ps
|
|
log ""
|
|
log "Recent logs (last 20 lines):"
|
|
compose logs --tail 20 2>/dev/null || true
|
|
}
|
|
|
|
do_uninstall_linux() {
|
|
find_infra_dir
|
|
ensure_docker
|
|
|
|
log "Uninstalling Docker runner..."
|
|
compose down -v --rmi local 2>/dev/null || compose down -v
|
|
log "Docker runner removed (containers, volumes, local images)."
|
|
log ""
|
|
log "Note: The runner should auto-deregister from GitHub (ephemeral mode)."
|
|
log "If a stale runner remains, remove it manually:"
|
|
log " gh api -X DELETE repos/OWNER/REPO/actions/runners/RUNNER_ID"
|
|
}
|
|
|
|
# ===========================================================================
|
|
# Entry point — routes to macOS or Linux implementation
|
|
# ===========================================================================
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
|
|
local os
|
|
os="$(detect_os)"
|
|
|
|
case "$MODE" in
|
|
setup)
|
|
if [[ "$os" == "darwin" ]]; then do_setup_darwin; else do_setup_linux; fi ;;
|
|
start)
|
|
if [[ "$os" == "darwin" ]]; then do_start_darwin; else do_start_linux; fi ;;
|
|
stop)
|
|
if [[ "$os" == "darwin" ]]; then do_stop_darwin; else do_stop_linux; fi ;;
|
|
status)
|
|
if [[ "$os" == "darwin" ]]; then do_status_darwin; else do_status_linux; fi ;;
|
|
build-image)
|
|
do_build_image ;;
|
|
uninstall)
|
|
if [[ "$os" == "darwin" ]]; then do_uninstall_darwin; else do_uninstall_linux; fi ;;
|
|
*)
|
|
die "unexpected mode: $MODE" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|