497 lines
12 KiB
Bash
Executable File
497 lines
12 KiB
Bash
Executable File
#!/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 <setup|start|stop> [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" <<EOF
|
|
runtime=$runtime
|
|
profile=$COLIMA_PROFILE
|
|
started_by_script=$started
|
|
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
EOF
|
|
}
|
|
|
|
read_state() {
|
|
STATE_RUNTIME=""
|
|
STATE_PROFILE=""
|
|
STATE_STARTED_BY_SCRIPT="0"
|
|
|
|
[[ -f "$STATE_FILE" ]] || return 0
|
|
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
runtime) STATE_RUNTIME="$value" ;;
|
|
profile) STATE_PROFILE="$value" ;;
|
|
started_by_script) STATE_STARTED_BY_SCRIPT="$value" ;;
|
|
esac
|
|
done < "$STATE_FILE"
|
|
}
|
|
|
|
resolve_runtime_auto() {
|
|
if command -v colima >/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 "$@"
|