feat: add runner conversion scripts and strengthen cutover automation

This commit is contained in:
S
2026-03-04 13:32:06 -06:00
parent e624885bb9
commit c2087d5087
43 changed files with 6995 additions and 42 deletions

View File

@@ -0,0 +1,496 @@
#!/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 "$@"