feat: add runner conversion scripts and strengthen cutover automation
This commit is contained in:
730
runners-conversion/periodVault/runner.sh
Executable file
730
runners-conversion/periodVault/runner.sh
Executable file
@@ -0,0 +1,730 @@
|
||||
#!/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="${PERIODVAULT_RUNNER_DIR:-${HOME}/.periodvault-runner}"
|
||||
RUNNER_LABELS="self-hosted,macOS,periodvault"
|
||||
RUNNER_NAME=""
|
||||
REPO_SLUG=""
|
||||
REG_TOKEN=""
|
||||
FORCE=false
|
||||
FOREGROUND=false
|
||||
PUSH_REGISTRY=""
|
||||
BUILD_TARGET=""
|
||||
|
||||
PLIST_LABEL="com.periodvault.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: ~/.periodvault-runner)
|
||||
--labels LABELS Comma-separated labels (default: self-hosted,macOS,periodvault)
|
||||
--name NAME Runner name (default: periodvault-<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):
|
||||
--target TARGET Dockerfile target: slim or full (default: builds both)
|
||||
--push REGISTRY Tag and push to a registry (e.g. localhost: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 runners
|
||||
./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 slim + full
|
||||
./scripts/runner.sh --mode build-image --target slim # build slim only
|
||||
./scripts/runner.sh --mode build-image --push localhost:5000 # build + push to local registry
|
||||
|
||||
Environment overrides:
|
||||
PERIODVAULT_RUNNER_DIR Runner installation directory (macOS only)
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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."
|
||||
}
|
||||
|
||||
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 periodvault 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 ;;
|
||||
--target)
|
||||
shift; [[ $# -gt 0 ]] || die "--target requires a value (slim or full)"
|
||||
BUILD_TARGET="$1"; shift ;;
|
||||
--force)
|
||||
FORCE=true; shift ;;
|
||||
--foreground)
|
||||
FOREGROUND=true; shift ;;
|
||||
--push)
|
||||
shift; [[ $# -gt 0 ]] || die "--push requires a registry address (e.g. localhost: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
|
||||
|
||||
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
|
||||
# ===========================================================================
|
||||
|
||||
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}"
|
||||
|
||||
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)."
|
||||
}
|
||||
|
||||
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="periodvault-$(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 these repository variables:"
|
||||
log ' CI_RUNS_ON_MACOS: ["self-hosted", "macOS", "periodvault"]'
|
||||
log ""
|
||||
log "Via CLI:"
|
||||
log ' gh variable set CI_RUNS_ON_MACOS --body '"'"'["self-hosted","macOS","periodvault"]'"'"
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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..."
|
||||
|
||||
unload_service
|
||||
|
||||
if [[ -f "$PLIST_PATH" ]]; then
|
||||
rm -f "$PLIST_PATH"
|
||||
log "Removed plist: $PLIST_PATH"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
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() {
|
||||
require_cmd docker
|
||||
|
||||
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/"
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose -f "${INFRA_DIR}/docker-compose.yml" "$@"
|
||||
}
|
||||
|
||||
do_build_image() {
|
||||
find_infra_dir
|
||||
ensure_docker
|
||||
|
||||
local targets=()
|
||||
if [[ -n "$BUILD_TARGET" ]]; then
|
||||
targets+=("$BUILD_TARGET")
|
||||
else
|
||||
targets+=("slim" "full")
|
||||
fi
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
local image_tag="periodvault-runner:${target}"
|
||||
if [[ -n "$PUSH_REGISTRY" ]]; then
|
||||
image_tag="${PUSH_REGISTRY}/periodvault-runner:${target}"
|
||||
fi
|
||||
|
||||
log "Building runner image: ${image_tag} (target: ${target}, platform: linux/amd64)"
|
||||
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 --pull \
|
||||
--target "$target" \
|
||||
-t "$image_tag" \
|
||||
"$INFRA_DIR"
|
||||
|
||||
if [[ -n "$PUSH_REGISTRY" ]]; then
|
||||
log "Pushing ${image_tag}..."
|
||||
docker push "$image_tag"
|
||||
log "Image pushed: ${image_tag}"
|
||||
else
|
||||
log "Image built locally: ${image_tag}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$PUSH_REGISTRY" ]]; then
|
||||
log ""
|
||||
log "Use --push <registry> to push to a registry."
|
||||
log "Example: ./scripts/runner.sh --mode build-image --push localhost:5000"
|
||||
fi
|
||||
}
|
||||
|
||||
do_setup_linux() {
|
||||
find_infra_dir
|
||||
ensure_docker
|
||||
|
||||
log "Docker-based runner setup (infra/runners/)"
|
||||
log ""
|
||||
|
||||
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
|
||||
|
||||
if [[ ! -f "${INFRA_DIR}/envs/periodvault.env" ]]; then
|
||||
if [[ -f "${INFRA_DIR}/envs/periodvault.env.example" ]]; then
|
||||
cp "${INFRA_DIR}/envs/periodvault.env.example" "${INFRA_DIR}/envs/periodvault.env"
|
||||
log "Created ${INFRA_DIR}/envs/periodvault.env from template."
|
||||
log "Edit this file to configure REPO_URL, RUNNER_NAME, and resource limits."
|
||||
else
|
||||
die "Missing envs/periodvault.env.example template in ${INFRA_DIR}"
|
||||
fi
|
||||
else
|
||||
log "envs/periodvault.env already exists; skipping."
|
||||
fi
|
||||
|
||||
log ""
|
||||
log "Starting runners..."
|
||||
compose up -d
|
||||
|
||||
log ""
|
||||
log "Setup complete. Verify with: ./scripts/runner.sh --mode status"
|
||||
log ""
|
||||
log "To activate self-hosted CI, set these repository variables:"
|
||||
log ' gh variable set CI_RUNS_ON --body '"'"'["self-hosted","Linux","X64"]'"'"
|
||||
log ' gh variable set CI_RUNS_ON_ANDROID --body '"'"'["self-hosted","Linux","X64","android-emulator"]'"'"
|
||||
}
|
||||
|
||||
do_start_linux() {
|
||||
find_infra_dir
|
||||
ensure_docker
|
||||
|
||||
log "Starting Docker runners..."
|
||||
compose up -d
|
||||
log "Runners started."
|
||||
}
|
||||
|
||||
do_stop_linux() {
|
||||
find_infra_dir
|
||||
ensure_docker
|
||||
|
||||
log "Stopping Docker runners..."
|
||||
compose down
|
||||
log "Runners 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 runners..."
|
||||
compose down -v --rmi local 2>/dev/null || compose down -v
|
||||
log "Docker runners removed (containers, volumes, local images)."
|
||||
log ""
|
||||
log "Note: Runners should auto-deregister from GitHub (ephemeral mode)."
|
||||
log "If stale runners remain, remove them manually:"
|
||||
log " gh api -X DELETE repos/OWNER/REPO/actions/runners/RUNNER_ID"
|
||||
}
|
||||
|
||||
# ===========================================================================
|
||||
# Entry point
|
||||
# ===========================================================================
|
||||
|
||||
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 "$@"
|
||||
Reference in New Issue
Block a user