#!/usr/bin/env bash # actions-local.sh — Setup/start/stop local GitHub Actions runtime on macOS. # # This script prepares and manages local execution of workflows with `act`. # Default runtime is Colima (free, local Docker daemon). # # Typical flow: # 1) ./scripts/actions-local.sh --mode setup # 2) ./scripts/actions-local.sh --mode start # 3) act -W .github/workflows/ci-quality-gates.yml # 4) ./scripts/actions-local.sh --mode stop set -euo pipefail MODE="" RUNTIME="auto" RUNTIME_EXPLICIT=false REFRESH_BREW=false COLIMA_PROFILE="${AUGUR_ACTIONS_COLIMA_PROFILE:-augur-actions}" COLIMA_CPU="${AUGUR_ACTIONS_COLIMA_CPU:-4}" COLIMA_MEMORY_GB="${AUGUR_ACTIONS_COLIMA_MEMORY_GB:-8}" COLIMA_DISK_GB="${AUGUR_ACTIONS_COLIMA_DISK_GB:-60}" WAIT_TIMEOUT_SEC="${AUGUR_ACTIONS_WAIT_TIMEOUT_SEC:-180}" STATE_DIR="${TMPDIR:-/tmp}" STATE_FILE="${STATE_DIR%/}/augur-actions-local.state" STATE_RUNTIME="" STATE_PROFILE="" STATE_STARTED_BY_SCRIPT="0" usage() { cat <<'EOF' Usage: ./scripts/actions-local.sh --mode [options] Required: --mode MODE One of: setup, start, stop Options: --runtime RUNTIME Runtime choice: auto, colima, docker-desktop (default: auto) --refresh-brew In setup mode, force brew metadata refresh even if nothing is missing --colima-profile NAME Colima profile name (default: augur-actions) --cpu N Colima CPU count for start (default: 4) --memory-gb N Colima memory (GB) for start (default: 8) --disk-gb N Colima disk (GB) for start (default: 60) -h, --help Show this help Examples: ./scripts/actions-local.sh --mode setup ./scripts/actions-local.sh --mode start ./scripts/actions-local.sh --mode start --runtime colima --cpu 6 --memory-gb 12 ./scripts/actions-local.sh --mode stop ./scripts/actions-local.sh --mode stop --runtime colima Environment overrides: AUGUR_ACTIONS_COLIMA_PROFILE AUGUR_ACTIONS_COLIMA_CPU AUGUR_ACTIONS_COLIMA_MEMORY_GB AUGUR_ACTIONS_COLIMA_DISK_GB AUGUR_ACTIONS_WAIT_TIMEOUT_SEC EOF } log() { printf '[actions-local] %s\n' "$*" } warn() { printf '[actions-local] WARNING: %s\n' "$*" >&2 } die() { printf '[actions-local] ERROR: %s\n' "$*" >&2 exit 1 } require_cmd() { local cmd="$1" command -v "$cmd" >/dev/null 2>&1 || die "required command not found: $cmd" } ensure_macos() { local os os="$(uname -s)" [[ "$os" == "Darwin" ]] || die "This script currently supports macOS only." } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --mode) shift [[ $# -gt 0 ]] || die "--mode requires a value" MODE="$1" shift ;; --runtime) shift [[ $# -gt 0 ]] || die "--runtime requires a value" RUNTIME="$1" RUNTIME_EXPLICIT=true shift ;; --refresh-brew) REFRESH_BREW=true shift ;; --colima-profile) shift [[ $# -gt 0 ]] || die "--colima-profile requires a value" COLIMA_PROFILE="$1" shift ;; --cpu) shift [[ $# -gt 0 ]] || die "--cpu requires a value" COLIMA_CPU="$1" shift ;; --memory-gb) shift [[ $# -gt 0 ]] || die "--memory-gb requires a value" COLIMA_MEMORY_GB="$1" shift ;; --disk-gb) shift [[ $# -gt 0 ]] || die "--disk-gb requires a value" COLIMA_DISK_GB="$1" shift ;; -h|--help) usage exit 0 ;; *) die "unknown argument: $1" ;; esac done [[ -n "$MODE" ]] || die "--mode is required (setup|start|stop)" case "$MODE" in setup|start|stop) ;; *) die "invalid --mode: $MODE (expected setup|start|stop)" ;; esac case "$RUNTIME" in auto|colima|docker-desktop) ;; *) die "invalid --runtime: $RUNTIME (expected auto|colima|docker-desktop)" ;; esac } ensure_command_line_tools() { if xcode-select -p >/dev/null 2>&1; then log "Xcode Command Line Tools already installed." return fi log "Xcode Command Line Tools missing; attempting automated install..." local marker="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress" local label="" touch "$marker" label="$(softwareupdate -l 2>/dev/null | sed -n 's/^\* Label: //p' | grep 'Command Line Tools' | tail -n1 || true)" rm -f "$marker" if [[ -n "$label" ]]; then sudo softwareupdate -i "$label" --verbose sudo xcode-select --switch /Library/Developer/CommandLineTools else warn "Could not auto-detect Command Line Tools package; launching GUI installer." xcode-select --install || true die "Finish installing Command Line Tools, then re-run setup." fi xcode-select -p >/dev/null 2>&1 || die "Command Line Tools installation did not complete." log "Xcode Command Line Tools installed." } ensure_homebrew() { if command -v brew >/dev/null 2>&1; then log "Homebrew already installed." else require_cmd curl log "Installing Homebrew..." NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" fi if [[ -x /opt/homebrew/bin/brew ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [[ -x /usr/local/bin/brew ]]; then eval "$(/usr/local/bin/brew shellenv)" elif command -v brew >/dev/null 2>&1; then eval "$("$(command -v brew)" shellenv)" else die "Homebrew not found after installation." fi log "Homebrew ready: $(brew --version | head -n1)" } install_brew_formula_if_missing() { local formula="$1" if brew list --versions "$formula" >/dev/null 2>&1; then log "Already installed: $formula" else log "Installing: $formula" brew install "$formula" fi } list_missing_formulas() { local formulas=("$@") local -a missing=() local formula for formula in "${formulas[@]}"; do if ! brew list --versions "$formula" >/dev/null 2>&1; then missing+=("$formula") fi done if [[ "${#missing[@]}" -gt 0 ]]; then printf '%s\n' "${missing[@]}" fi } colima_context_name() { local profile="$1" if [[ "$profile" == "default" ]]; then printf 'colima' else printf 'colima-%s' "$profile" fi } colima_is_running() { local out out="$(colima status --profile "$COLIMA_PROFILE" 2>&1 || true)" if printf '%s' "$out" | grep -qi "not running"; then return 1 fi if printf '%s' "$out" | grep -qi "running"; then return 0 fi return 1 } docker_ready() { docker info >/dev/null 2>&1 } wait_for_docker() { local waited=0 while ! docker_ready; do if (( waited >= WAIT_TIMEOUT_SEC )); then die "Docker daemon not ready after ${WAIT_TIMEOUT_SEC}s." fi sleep 2 waited=$((waited + 2)) done } write_state() { local runtime="$1" local started="$2" cat > "$STATE_FILE" </dev/null 2>&1; then printf 'colima' return fi if [[ -d "/Applications/Docker.app" ]] || command -v docker >/dev/null 2>&1; then printf 'docker-desktop' return fi die "No supported runtime found. Run setup first." } start_colima_runtime() { require_cmd colima require_cmd docker require_cmd act local started="0" if colima_is_running; then log "Colima profile '${COLIMA_PROFILE}' is already running." else log "Starting Colima profile '${COLIMA_PROFILE}' (cpu=${COLIMA_CPU}, memory=${COLIMA_MEMORY_GB}GB, disk=${COLIMA_DISK_GB}GB)..." colima start --profile "$COLIMA_PROFILE" --cpu "$COLIMA_CPU" --memory "$COLIMA_MEMORY_GB" --disk "$COLIMA_DISK_GB" started="1" fi local context context="$(colima_context_name "$COLIMA_PROFILE")" if docker context ls --format '{{.Name}}' | grep -Fxq "$context"; then docker context use "$context" >/dev/null 2>&1 || true fi wait_for_docker write_state "colima" "$started" log "Runtime ready (colima)." log "Try: act -W .github/workflows/ci-quality-gates.yml" } start_docker_desktop_runtime() { require_cmd docker require_cmd act require_cmd open local started="0" if docker_ready; then log "Docker daemon already running." else log "Starting Docker Desktop..." open -ga Docker started="1" fi wait_for_docker write_state "docker-desktop" "$started" log "Runtime ready (docker-desktop)." log "Try: act -W .github/workflows/ci-quality-gates.yml" } stop_colima_runtime() { require_cmd colima if colima_is_running; then log "Stopping Colima profile '${COLIMA_PROFILE}'..." colima stop --profile "$COLIMA_PROFILE" else log "Colima profile '${COLIMA_PROFILE}' is already stopped." fi } stop_docker_desktop_runtime() { require_cmd osascript log "Stopping Docker Desktop..." osascript -e 'quit app "Docker"' >/dev/null 2>&1 || true } do_setup() { ensure_macos ensure_command_line_tools ensure_homebrew local required_formulas=(git act colima docker) local missing_formulas=() local missing_formula while IFS= read -r missing_formula; do [[ -n "$missing_formula" ]] || continue missing_formulas+=("$missing_formula") done < <(list_missing_formulas "${required_formulas[@]}" || true) if [[ "${#missing_formulas[@]}" -eq 0 ]]; then log "All required formulas already installed: ${required_formulas[*]}" if [[ "$REFRESH_BREW" == "true" ]]; then log "Refreshing Homebrew metadata (--refresh-brew)..." brew update else log "Skipping brew update; nothing to install." fi log "Setup complete (no changes required)." log "Next: ./scripts/actions-local.sh --mode start" return fi log "Missing formulas detected: ${missing_formulas[*]}" log "Updating Homebrew metadata..." brew update local formula for formula in "${required_formulas[@]}"; do install_brew_formula_if_missing "$formula" done log "Setup complete." log "Next: ./scripts/actions-local.sh --mode start" } do_start() { ensure_macos local selected_runtime="$RUNTIME" if [[ "$selected_runtime" == "auto" ]]; then selected_runtime="$(resolve_runtime_auto)" fi case "$selected_runtime" in colima) start_colima_runtime ;; docker-desktop) start_docker_desktop_runtime ;; *) die "unsupported runtime: $selected_runtime" ;; esac } do_stop() { ensure_macos read_state local selected_runtime="$RUNTIME" local should_stop="1" if [[ "$selected_runtime" == "auto" ]]; then if [[ -n "$STATE_RUNTIME" ]]; then selected_runtime="$STATE_RUNTIME" if [[ -n "$STATE_PROFILE" ]]; then COLIMA_PROFILE="$STATE_PROFILE" fi if [[ "$STATE_STARTED_BY_SCRIPT" != "1" ]]; then should_stop="0" fi else if command -v colima >/dev/null 2>&1; then selected_runtime="colima" elif [[ -d "/Applications/Docker.app" ]] || command -v docker >/dev/null 2>&1; then selected_runtime="docker-desktop" else log "No local Actions runtime is installed or tracked. Nothing to stop." return fi should_stop="0" fi fi if [[ "$should_stop" != "1" && "$RUNTIME_EXPLICIT" != "true" ]]; then log "No runtime started by this script is currently tracked. Nothing to stop." log "Pass --runtime colima or --runtime docker-desktop to force a stop." return fi case "$selected_runtime" in colima) stop_colima_runtime ;; docker-desktop) stop_docker_desktop_runtime ;; *) die "unsupported runtime: $selected_runtime" ;; esac if [[ -f "$STATE_FILE" ]]; then rm -f "$STATE_FILE" fi log "Stop complete." } main() { parse_args "$@" case "$MODE" in setup) do_setup ;; start) do_start ;; stop) do_stop ;; *) die "unexpected mode: $MODE" ;; esac } main "$@"