Streamline developer onboarding with a single bootstrap.sh that checks prerequisites, configures .env with generated secrets, installs deps, starts Docker services, and runs migrations + seeding. Co-Authored-By: Paperclip <noreply@paperclip.ing>
298 lines
11 KiB
Bash
Executable File
298 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# GoodGo Platform — One-Command Bootstrap
|
||
# =============================================================================
|
||
# Sets up a complete local development environment from a clean clone.
|
||
#
|
||
# Usage:
|
||
# ./scripts/bootstrap.sh # Full setup
|
||
# ./scripts/bootstrap.sh --skip-seed # Skip database seeding
|
||
# ./scripts/bootstrap.sh --no-docker # Skip Docker services (BYO database)
|
||
#
|
||
# Idempotent — safe to run multiple times.
|
||
# Works on macOS and Linux.
|
||
# =============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
# ── Colours & Helpers ────────────────────────────────────────────────────────
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m' # No Color
|
||
|
||
info() { printf "${BLUE}ℹ${NC} %s\n" "$1"; }
|
||
success() { printf "${GREEN}✔${NC} %s\n" "$1"; }
|
||
warn() { printf "${YELLOW}⚠${NC} %s\n" "$1"; }
|
||
fail() { printf "${RED}✖${NC} %s\n" "$1" >&2; exit 1; }
|
||
step() { printf "\n${BOLD}── %s ──${NC}\n" "$1"; }
|
||
|
||
# ── Parse Arguments ──────────────────────────────────────────────────────────
|
||
|
||
SKIP_SEED=false
|
||
NO_DOCKER=false
|
||
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--skip-seed) SKIP_SEED=true ;;
|
||
--no-docker) NO_DOCKER=true ;;
|
||
--help|-h)
|
||
echo "Usage: ./scripts/bootstrap.sh [--skip-seed] [--no-docker]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --skip-seed Skip database seeding (pnpm db:seed)"
|
||
echo " --no-docker Skip Docker Compose services (bring your own DB)"
|
||
echo " --help, -h Show this help message"
|
||
exit 0
|
||
;;
|
||
*)
|
||
warn "Unknown argument: $arg (ignored)"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ── Navigate to Project Root ─────────────────────────────────────────────────
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
cd "$PROJECT_ROOT"
|
||
|
||
echo ""
|
||
printf "${BOLD}╔══════════════════════════════════════════════════╗${NC}\n"
|
||
printf "${BOLD}║ GoodGo Platform — Developer Bootstrap ║${NC}\n"
|
||
printf "${BOLD}╚══════════════════════════════════════════════════╝${NC}\n"
|
||
echo ""
|
||
info "Project root: $PROJECT_ROOT"
|
||
|
||
# ── Step 1: Check Prerequisites ──────────────────────────────────────────────
|
||
|
||
step "Checking prerequisites"
|
||
|
||
MISSING=()
|
||
|
||
# Node.js >= 22
|
||
if command -v node &>/dev/null; then
|
||
NODE_VERSION=$(node -v | sed 's/v//')
|
||
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1)
|
||
if [ "$NODE_MAJOR" -ge 22 ]; then
|
||
success "Node.js $NODE_VERSION (>= 22 required)"
|
||
else
|
||
fail "Node.js $NODE_VERSION found but >= 22 is required. Install via nvm: nvm install 22"
|
||
fi
|
||
else
|
||
MISSING+=("Node.js >= 22 — https://nodejs.org or 'nvm install 22'")
|
||
fi
|
||
|
||
# pnpm
|
||
if command -v pnpm &>/dev/null; then
|
||
PNPM_VERSION=$(pnpm -v)
|
||
success "pnpm $PNPM_VERSION"
|
||
else
|
||
MISSING+=("pnpm — install with 'corepack enable' or 'npm install -g pnpm'")
|
||
fi
|
||
|
||
# Docker (only if not --no-docker)
|
||
if [ "$NO_DOCKER" = false ]; then
|
||
if command -v docker &>/dev/null; then
|
||
DOCKER_VERSION=$(docker --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||
success "Docker $DOCKER_VERSION"
|
||
else
|
||
MISSING+=("Docker — https://docs.docker.com/get-docker/")
|
||
fi
|
||
|
||
# Docker Compose (v2 plugin or standalone)
|
||
if docker compose version &>/dev/null 2>&1; then
|
||
COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || docker compose version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||
success "Docker Compose $COMPOSE_VERSION"
|
||
elif command -v docker-compose &>/dev/null; then
|
||
COMPOSE_VERSION=$(docker-compose --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||
success "Docker Compose $COMPOSE_VERSION (standalone)"
|
||
else
|
||
MISSING+=("Docker Compose — https://docs.docker.com/compose/install/")
|
||
fi
|
||
|
||
# Check Docker daemon is running
|
||
if ! docker info &>/dev/null 2>&1; then
|
||
fail "Docker daemon is not running. Please start Docker Desktop or the Docker service."
|
||
fi
|
||
fi
|
||
|
||
if [ ${#MISSING[@]} -gt 0 ]; then
|
||
echo ""
|
||
printf "${RED}Missing prerequisites:${NC}\n"
|
||
for dep in "${MISSING[@]}"; do
|
||
echo " • $dep"
|
||
done
|
||
echo ""
|
||
fail "Install the missing tools above and re-run this script."
|
||
fi
|
||
|
||
success "All prerequisites met"
|
||
|
||
# ── Step 2: Environment File ─────────────────────────────────────────────────
|
||
|
||
step "Setting up environment"
|
||
|
||
if [ -f .env ]; then
|
||
success ".env already exists (skipping copy)"
|
||
else
|
||
if [ -f .env.example ]; then
|
||
cp .env.example .env
|
||
|
||
# Generate secure defaults for development
|
||
if command -v openssl &>/dev/null; then
|
||
JWT_SECRET_VAL=$(openssl rand -base64 48)
|
||
JWT_REFRESH_VAL=$(openssl rand -base64 48)
|
||
KYC_KEY_VAL=$(openssl rand -hex 32)
|
||
|
||
# Platform-safe sed in-place (macOS vs Linux)
|
||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||
SED_INPLACE=(sed -i '')
|
||
else
|
||
SED_INPLACE=(sed -i)
|
||
fi
|
||
|
||
"${SED_INPLACE[@]}" "s|DB_PASSWORD=CHANGE_ME|DB_PASSWORD=goodgo_secret|" .env
|
||
"${SED_INPLACE[@]}" "s|TYPESENSE_API_KEY=CHANGE_ME|TYPESENSE_API_KEY=ts_dev_key_change_me|" .env
|
||
"${SED_INPLACE[@]}" "s|MINIO_ACCESS_KEY=CHANGE_ME|MINIO_ACCESS_KEY=minioadmin|" .env
|
||
"${SED_INPLACE[@]}" "s|MINIO_SECRET_KEY=CHANGE_ME|MINIO_SECRET_KEY=minioadmin|" .env
|
||
"${SED_INPLACE[@]}" "s|PGBOUNCER_ADMIN_PASSWORD=CHANGE_ME|PGBOUNCER_ADMIN_PASSWORD=pgbouncer_admin|" .env
|
||
"${SED_INPLACE[@]}" "s|PGBOUNCER_STATS_PASSWORD=CHANGE_ME|PGBOUNCER_STATS_PASSWORD=pgbouncer_stats|" .env
|
||
"${SED_INPLACE[@]}" "s|JWT_SECRET=.*|JWT_SECRET=$JWT_SECRET_VAL|" .env
|
||
"${SED_INPLACE[@]}" "s|JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$JWT_REFRESH_VAL|" .env
|
||
"${SED_INPLACE[@]}" "s|KYC_ENCRYPTION_KEY=.*|KYC_ENCRYPTION_KEY=$KYC_KEY_VAL|" .env
|
||
else
|
||
warn "openssl not found — .env copied but secrets are placeholders. Update them manually."
|
||
fi
|
||
|
||
success ".env created from .env.example with generated dev secrets"
|
||
else
|
||
fail ".env.example not found. Is this the project root?"
|
||
fi
|
||
fi
|
||
|
||
# ── Step 3: Install Dependencies ─────────────────────────────────────────────
|
||
|
||
step "Installing dependencies"
|
||
|
||
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||
success "Dependencies installed"
|
||
|
||
# ── Step 4: Start Docker Services ────────────────────────────────────────────
|
||
|
||
if [ "$NO_DOCKER" = false ]; then
|
||
step "Starting Docker services"
|
||
|
||
# Start only core dev services (not monitoring/backup stack)
|
||
CORE_SERVICES=(postgres redis typesense minio)
|
||
|
||
info "Starting: ${CORE_SERVICES[*]}"
|
||
docker compose up -d "${CORE_SERVICES[@]}"
|
||
|
||
# Wait for services to be healthy
|
||
info "Waiting for services to be healthy..."
|
||
|
||
MAX_WAIT=120
|
||
ELAPSED=0
|
||
INTERVAL=5
|
||
|
||
wait_for_service() {
|
||
local service="$1"
|
||
local container="goodgo-$service"
|
||
|
||
while [ "$ELAPSED" -lt "$MAX_WAIT" ]; do
|
||
local health
|
||
health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "not_found")
|
||
|
||
case "$health" in
|
||
healthy)
|
||
success "$service is healthy"
|
||
return 0
|
||
;;
|
||
unhealthy)
|
||
fail "$service is unhealthy. Check logs: docker compose logs $service"
|
||
;;
|
||
not_found)
|
||
fail "Container $container not found. Check: docker compose ps"
|
||
;;
|
||
*)
|
||
# starting or no status yet
|
||
;;
|
||
esac
|
||
|
||
sleep "$INTERVAL"
|
||
ELAPSED=$((ELAPSED + INTERVAL))
|
||
done
|
||
|
||
fail "$service did not become healthy within ${MAX_WAIT}s. Check logs: docker compose logs $service"
|
||
}
|
||
|
||
for svc in "${CORE_SERVICES[@]}"; do
|
||
ELAPSED=0
|
||
wait_for_service "$svc"
|
||
done
|
||
|
||
success "All Docker services are healthy"
|
||
else
|
||
warn "Skipping Docker services (--no-docker). Ensure PostgreSQL, Redis, Typesense, and MinIO are available."
|
||
fi
|
||
|
||
# ── Step 5: Generate Prisma Client ───────────────────────────────────────────
|
||
|
||
step "Setting up database"
|
||
|
||
info "Generating Prisma client..."
|
||
pnpm db:generate
|
||
success "Prisma client generated"
|
||
|
||
# ── Step 6: Run Migrations ───────────────────────────────────────────────────
|
||
|
||
info "Running database migrations..."
|
||
pnpm db:migrate:dev --name init 2>/dev/null || pnpm db:migrate:dev 2>/dev/null || {
|
||
# If migrate dev fails (e.g., migrations already applied), try deploy instead
|
||
warn "migrate dev had issues, trying migrate deploy..."
|
||
pnpm db:migrate:deploy
|
||
}
|
||
success "Database migrations applied"
|
||
|
||
# ── Step 7: Seed Database ────────────────────────────────────────────────────
|
||
|
||
if [ "$SKIP_SEED" = false ]; then
|
||
info "Seeding database..."
|
||
pnpm db:seed || {
|
||
warn "Seed command returned non-zero (data may already exist). Continuing..."
|
||
}
|
||
success "Database seeded"
|
||
else
|
||
info "Skipping database seed (--skip-seed)"
|
||
fi
|
||
|
||
# ── Done! ────────────────────────────────────────────────────────────────────
|
||
|
||
echo ""
|
||
printf "${BOLD}${GREEN}╔══════════════════════════════════════════════════╗${NC}\n"
|
||
printf "${BOLD}${GREEN}║ Setup Complete! ║${NC}\n"
|
||
printf "${BOLD}${GREEN}╚══════════════════════════════════════════════════╝${NC}\n"
|
||
echo ""
|
||
echo "Next steps:"
|
||
echo ""
|
||
echo " ${BOLD}pnpm dev${NC} Start all apps (API :3001, Web :3000)"
|
||
echo " ${BOLD}pnpm db:studio${NC} Open Prisma Studio to browse data"
|
||
echo " ${BOLD}pnpm test${NC} Run unit tests"
|
||
echo " ${BOLD}pnpm lint${NC} Run ESLint"
|
||
echo ""
|
||
echo "Optional services (monitoring stack):"
|
||
echo ""
|
||
echo " ${BOLD}docker compose up -d loki promtail prometheus grafana${NC}"
|
||
echo " Grafana: http://localhost:3002 (admin/admin)"
|
||
echo ""
|
||
echo "Documentation:"
|
||
echo ""
|
||
echo " README.md Project overview"
|
||
echo " CONTRIBUTING.md Contribution guidelines"
|
||
echo " CLAUDE.md AI assistant instructions"
|
||
echo ""
|