feat: add setup.sh for idempotent local dev environment setup
Automates the full first-time setup in one command (also safe to re-run): - Checks docker/pnpm; installs just if missing - Adds 127.0.0.1 keycloak to /etc/hosts (sudo) - Generates .env with dev JWT tokens computed from the dev secret - Fixes supabase/migrations permissions if root-owned - Starts the Docker stack and waits for postgres/kong/keycloak - Applies SQL migrations with tracking table (won't re-apply) - Seeds the database (no-op if already seeded) - Compiles Paraglide i18n output Also adds `just setup` recipe and fixes the dev anon key in .env.development. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
6
Justfile
6
Justfile
@@ -7,6 +7,12 @@ set dotenv-load := true
|
|||||||
dc_dev := "docker compose --env-file .env -f infra/docker-compose.dev.yml"
|
dc_dev := "docker compose --env-file .env -f infra/docker-compose.dev.yml"
|
||||||
dc_prod := "docker compose --env-file .env -f infra/docker-compose.prod.yml"
|
dc_prod := "docker compose --env-file .env -f infra/docker-compose.prod.yml"
|
||||||
|
|
||||||
|
# ── Setup ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# First-time (and idempotent) local environment setup
|
||||||
|
setup:
|
||||||
|
bash setup.sh
|
||||||
|
|
||||||
# ── Dev ───────────────────────────────────────────────────────────────────────
|
# ── Dev ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Start full local stack (Docker services + SvelteKit dev server)
|
# Start full local stack (Docker services + SvelteKit dev server)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Default public env vars for local development.
|
# Default public env vars for local development.
|
||||||
# Committed — these are not secrets. Secret vars (.env) remain gitignored.
|
# Committed — these are not secrets. Secret vars (.env) remain gitignored.
|
||||||
PUBLIC_SUPABASE_URL=http://localhost:8001
|
PUBLIC_SUPABASE_URL=http://localhost:8001
|
||||||
PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNDc0MDAwMCwiZXhwIjo0NzkwNDEzNjAwfQ.F70ylwDTR13K6X_jOWkGFdYNdXrQlzIFz6gbN4aqaMM
|
PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNDc0MDAwMCwiZXhwIjo0NzkwNDEzNjAwfQ.NRJAsCq6gp4kkP2f7bMxQmfbFWrZ6xy7r8btkIMK_h4
|
||||||
PUBLIC_KEYCLOAK_URL=http://keycloak:8080
|
PUBLIC_KEYCLOAK_URL=http://keycloak:8080
|
||||||
PUBLIC_KEYCLOAK_REALM=colectivo
|
PUBLIC_KEYCLOAK_REALM=colectivo
|
||||||
PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web
|
PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web
|
||||||
|
|||||||
287
setup.sh
Executable file
287
setup.sh
Executable file
@@ -0,0 +1,287 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# setup.sh — Prepare the local development environment.
|
||||||
|
# Safe to run multiple times: every step is a no-op if already complete.
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Checks prerequisites (docker, pnpm); installs just if missing
|
||||||
|
# 2. Adds 127.0.0.1 keycloak to /etc/hosts (needed for GoTrue ↔ Keycloak)
|
||||||
|
# 3. Creates .env with dev defaults if it doesn't exist
|
||||||
|
# 4. Fixes supabase/migrations permissions if root-owned (common Docker artifact)
|
||||||
|
# 5. Installs pnpm dependencies
|
||||||
|
# 6. Starts the Docker stack
|
||||||
|
# 7. Waits for postgres, kong, and keycloak to be healthy
|
||||||
|
# 8. Applies SQL migrations (tracked — won't re-apply)
|
||||||
|
# 9. Seeds the database (no-op if already seeded)
|
||||||
|
# 10. Compiles Paraglide i18n files
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Colours ───────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
step() { echo; echo -e "${BOLD}${BLUE}==> $*${NC}"; }
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $*"; }
|
||||||
|
warn() { echo -e " ${YELLOW}!${NC} $*"; }
|
||||||
|
die() { echo -e "\n${RED}ERROR:${NC} $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── Resolve project root ──────────────────────────────────────────────────────
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
DC="docker compose --env-file .env -f infra/docker-compose.dev.yml"
|
||||||
|
DB_CONTAINER="colectivo-dev-db-1"
|
||||||
|
|
||||||
|
# ── 1. Prerequisites ──────────────────────────────────────────────────────────
|
||||||
|
step "Checking prerequisites"
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 \
|
||||||
|
|| die "docker not found. Install from https://docs.docker.com/get-docker/"
|
||||||
|
docker compose version >/dev/null 2>&1 \
|
||||||
|
|| die "docker compose v2 not found. Update Docker or install the compose plugin."
|
||||||
|
ok "docker $(docker --version | grep -oP '[\d.]+' | head -1)"
|
||||||
|
|
||||||
|
command -v pnpm >/dev/null 2>&1 \
|
||||||
|
|| die "pnpm not found. Install: curl -fsSL https://get.pnpm.io/install.sh | sh"
|
||||||
|
ok "pnpm $(pnpm --version)"
|
||||||
|
|
||||||
|
command -v python3 >/dev/null 2>&1 \
|
||||||
|
|| die "python3 not found — required to generate JWT tokens."
|
||||||
|
|
||||||
|
if ! command -v just >/dev/null 2>&1; then
|
||||||
|
warn "just not found — installing to /usr/local/bin (requires sudo)"
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \
|
||||||
|
| sudo bash -s -- --to /usr/local/bin
|
||||||
|
ok "just $(just --version)"
|
||||||
|
else
|
||||||
|
ok "just $(just --version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. /etc/hosts ─────────────────────────────────────────────────────────────
|
||||||
|
step "Checking /etc/hosts for 'keycloak' → 127.0.0.1"
|
||||||
|
|
||||||
|
if grep -qE '^\s*127\.0\.0\.1\s+.*\bkeycloak\b' /etc/hosts 2>/dev/null; then
|
||||||
|
ok "Entry already present"
|
||||||
|
else
|
||||||
|
warn "Adding '127.0.0.1 keycloak' to /etc/hosts (requires sudo)"
|
||||||
|
echo "127.0.0.1 keycloak" | sudo tee -a /etc/hosts >/dev/null
|
||||||
|
ok "Entry added"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. .env ────────────────────────────────────────────────────────────────────
|
||||||
|
step "Setting up .env"
|
||||||
|
|
||||||
|
if [[ -f .env ]]; then
|
||||||
|
ok ".env already exists — skipping"
|
||||||
|
else
|
||||||
|
warn ".env not found — generating with dev defaults"
|
||||||
|
|
||||||
|
# Well-known dev-only JWT secret. Never use in production.
|
||||||
|
JWT_SECRET="super-secret-jwt-token-with-at-least-32-characters-long"
|
||||||
|
|
||||||
|
# Generate anon and service_role JWTs signed with the dev secret.
|
||||||
|
# Python 3 is available on virtually every Linux/macOS dev machine.
|
||||||
|
IFS=' ' read -r ANON_KEY SERVICE_KEY < <(python3 - <<PYEOF
|
||||||
|
import hmac, hashlib, base64, json
|
||||||
|
|
||||||
|
secret = "${JWT_SECRET}"
|
||||||
|
|
||||||
|
def b64url(data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode()
|
||||||
|
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
|
||||||
|
|
||||||
|
header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(',', ':')))
|
||||||
|
|
||||||
|
def make_jwt(payload):
|
||||||
|
p = b64url(json.dumps(payload, separators=(',', ':')))
|
||||||
|
msg = f"{header}.{p}".encode()
|
||||||
|
sig = b64url(hmac.new(secret.encode(), msg, hashlib.sha256).digest())
|
||||||
|
return f"{header}.{p}.{sig}"
|
||||||
|
|
||||||
|
anon = make_jwt({"role": "anon", "iat": 1634740000, "exp": 4790413600})
|
||||||
|
srole = make_jwt({"role": "service_role", "iat": 1634740000, "exp": 4790413600})
|
||||||
|
print(anon, srole)
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
|
||||||
|
cat > .env <<ENVEOF
|
||||||
|
# Local development environment — generated by setup.sh
|
||||||
|
# Never commit this file. Override values here; do NOT edit .env.example.
|
||||||
|
|
||||||
|
# ── PostgreSQL ────────────────────────────────────────────────────────────────
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
|
||||||
|
# ── Supabase JWT — dev only, never use in production ─────────────────────────
|
||||||
|
SUPABASE_JWT_SECRET=${JWT_SECRET}
|
||||||
|
PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY}
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=${SERVICE_KEY}
|
||||||
|
|
||||||
|
# ── Keycloak ──────────────────────────────────────────────────────────────────
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
|
# ── Public endpoints ──────────────────────────────────────────────────────────
|
||||||
|
PUBLIC_SUPABASE_URL=http://localhost:8001
|
||||||
|
PUBLIC_KEYCLOAK_URL=http://keycloak:8080
|
||||||
|
PUBLIC_KEYCLOAK_REALM=colectivo
|
||||||
|
PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web
|
||||||
|
PUBLIC_APP_URL=http://localhost:5173
|
||||||
|
ENVEOF
|
||||||
|
|
||||||
|
ok ".env created (JWT tokens generated from dev secret)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. supabase/migrations permissions ────────────────────────────────────────
|
||||||
|
step "Checking supabase/migrations permissions"
|
||||||
|
|
||||||
|
mkdir -p supabase/migrations
|
||||||
|
|
||||||
|
if [[ ! -w supabase/migrations ]]; then
|
||||||
|
warn "supabase/migrations not writable — fixing with sudo chown"
|
||||||
|
sudo chown "$USER:$USER" supabase/migrations
|
||||||
|
ok "Permissions fixed"
|
||||||
|
else
|
||||||
|
ok "supabase/migrations is writable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. Install dependencies ────────────────────────────────────────────────────
|
||||||
|
step "Installing pnpm dependencies"
|
||||||
|
pnpm install
|
||||||
|
ok "Dependencies ready"
|
||||||
|
|
||||||
|
# ── 6. Start Docker stack ──────────────────────────────────────────────────────
|
||||||
|
step "Starting Docker stack"
|
||||||
|
$DC up -d
|
||||||
|
ok "Containers started"
|
||||||
|
|
||||||
|
# ── 7. Wait for services ───────────────────────────────────────────────────────
|
||||||
|
step "Waiting for services"
|
||||||
|
|
||||||
|
# Usage: wait_for <label> <check_command> [timeout_seconds]
|
||||||
|
wait_for() {
|
||||||
|
local label="$1"
|
||||||
|
local cmd="$2"
|
||||||
|
local max="${3:-90}"
|
||||||
|
local n=0
|
||||||
|
printf " %-18s" "$label"
|
||||||
|
until eval "$cmd" >/dev/null 2>&1; do
|
||||||
|
n=$((n + 2))
|
||||||
|
if (( n >= max )); then
|
||||||
|
echo -e " ${RED}timed out${NC}"
|
||||||
|
die "$label did not become healthy after ${max}s. Check: docker compose -f infra/docker-compose.dev.yml logs"
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
printf "."
|
||||||
|
done
|
||||||
|
echo -e " ${GREEN}ready${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL must be first — everything else depends on it.
|
||||||
|
wait_for "postgres" "docker exec ${DB_CONTAINER} pg_isready -U postgres -q"
|
||||||
|
# GoTrue (auth) is reached through Kong.
|
||||||
|
wait_for "kong/auth" "curl -sf http://localhost:8001/auth/v1/health"
|
||||||
|
# Keycloak realm endpoint — ready only after realm import completes.
|
||||||
|
wait_for "keycloak" "curl -sf http://localhost:8080/realms/colectivo" 120
|
||||||
|
|
||||||
|
# ── 8. Apply database migrations ──────────────────────────────────────────────
|
||||||
|
step "Applying database migrations"
|
||||||
|
|
||||||
|
# Create migration tracking table in the public schema (idempotent).
|
||||||
|
docker exec "$DB_CONTAINER" psql -U postgres -d postgres -q \
|
||||||
|
-c "CREATE TABLE IF NOT EXISTS public._applied_migrations (
|
||||||
|
filename TEXT PRIMARY KEY,
|
||||||
|
applied_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);" \
|
||||||
|
2>/dev/null || die "Could not create migration tracking table"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
migration_files=(supabase/migrations/*.sql)
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if (( ${#migration_files[@]} == 0 )); then
|
||||||
|
warn "No migration files found in supabase/migrations/"
|
||||||
|
else
|
||||||
|
for sql_file in "${migration_files[@]}"; do
|
||||||
|
filename="$(basename "$sql_file")"
|
||||||
|
|
||||||
|
already_applied=$(docker exec "$DB_CONTAINER" psql -U postgres -d postgres -tAq \
|
||||||
|
-c "SELECT count(*) FROM public._applied_migrations WHERE filename = '${filename}'" \
|
||||||
|
2>/dev/null)
|
||||||
|
|
||||||
|
if [[ "${already_applied:-0}" -gt 0 ]]; then
|
||||||
|
ok "$filename (already applied)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " Applying %-40s" "$filename ..."
|
||||||
|
if docker exec -i "$DB_CONTAINER" \
|
||||||
|
psql -U postgres -d postgres -q -v ON_ERROR_STOP=1 \
|
||||||
|
< "$sql_file" 2>/tmp/migration_err; then
|
||||||
|
docker exec "$DB_CONTAINER" psql -U postgres -d postgres -q \
|
||||||
|
-c "INSERT INTO public._applied_migrations (filename)
|
||||||
|
VALUES ('${filename}') ON CONFLICT DO NOTHING;"
|
||||||
|
echo -e " ${GREEN}done${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}FAILED${NC}"
|
||||||
|
echo "--- error output ---"
|
||||||
|
cat /tmp/migration_err
|
||||||
|
die "Migration ${filename} failed. Fix the error and re-run setup.sh."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 9. Seed database ───────────────────────────────────────────────────────────
|
||||||
|
step "Seeding database"
|
||||||
|
|
||||||
|
user_count=$(docker exec "$DB_CONTAINER" psql -U postgres -d postgres -tAq \
|
||||||
|
-c "SELECT count(*) FROM public.users" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if (( user_count > 0 )); then
|
||||||
|
ok "Already seeded ($user_count users in public.users)"
|
||||||
|
else
|
||||||
|
printf " Applying seed.sql ..."
|
||||||
|
if docker exec -i "$DB_CONTAINER" \
|
||||||
|
psql -U postgres -d postgres -q -v ON_ERROR_STOP=1 \
|
||||||
|
< supabase/seed.sql 2>/tmp/seed_err; then
|
||||||
|
echo -e " ${GREEN}done${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}FAILED${NC}"
|
||||||
|
cat /tmp/seed_err
|
||||||
|
die "seed.sql failed. Fix the error and re-run setup.sh."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 10. Compile Paraglide i18n ──────────────────────────────────────────────
|
||||||
|
step "Compiling Paraglide i18n"
|
||||||
|
|
||||||
|
PARAGLIDE_OUTDIR="apps/web/src/lib/paraglide"
|
||||||
|
|
||||||
|
if [[ -d "$PARAGLIDE_OUTDIR" && -f "$PARAGLIDE_OUTDIR/runtime.js" ]]; then
|
||||||
|
ok "Already compiled"
|
||||||
|
else
|
||||||
|
PARAGLIDE_BIN="$(find node_modules/.pnpm -name 'paraglide-js' -path '*/.bin/paraglide-js' \
|
||||||
|
2>/dev/null | head -1)"
|
||||||
|
|
||||||
|
if [[ -z "$PARAGLIDE_BIN" ]]; then
|
||||||
|
warn "paraglide-js binary not found — skipping (run 'pnpm install' and re-run setup.sh)"
|
||||||
|
else
|
||||||
|
"$PARAGLIDE_BIN" compile \
|
||||||
|
--project apps/web/project.inlang \
|
||||||
|
--outdir "$PARAGLIDE_OUTDIR"
|
||||||
|
ok "Compiled to $PARAGLIDE_OUTDIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Done ───────────────────────────────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo -e "${BOLD}${GREEN}✓ Local environment is ready.${NC}"
|
||||||
|
echo
|
||||||
|
echo " Start the app: just dev"
|
||||||
|
echo " Supabase Studio: http://localhost:54323"
|
||||||
|
echo " Keycloak Admin: http://localhost:8080/admin (admin / admin)"
|
||||||
|
echo " Test users (pw: test1234): ana, borja, carmen, david, eva"
|
||||||
|
echo
|
||||||
Reference in New Issue
Block a user