Files
Cibello-app/infrastructure/deployment/first-deploy.sh
T

285 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# FÖRSTA DEPLOY idempotent uppsättning av hela plattformen på en server.
#
# DEPLOY_MODE=staging ./infrastructure/deployment/first-deploy.sh
# DEPLOY_MODE=production ./infrastructure/deployment/first-deploy.sh
#
# STAGING (default): startar Postgres+Redis i Docker, genererar hemligheter
# om de saknas, AAMOS_MODE=mock tillåtet, seedar databasen, kör hela
# verifieringskedjan. Säker att köra om varje steg är idempotent.
#
# PRODUCTION: kräver befintlig .env med riktiga värden (skriptet VÄGRAR
# generera produktionshemligheter), kräver AAMOS_MODE=http, tar backup
# före migrationer, seedar ALDRIG automatiskt.
#
# Avslutar med exitkod 0 endast när ALLA grindar är gröna.
# ============================================================================
set -euo pipefail
MODE="${DEPLOY_MODE:-staging}"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
LOG_PREFIX="[first-deploy:$MODE]"
say() { echo "$LOG_PREFIX $*"; }
fail() { echo "$LOG_PREFIX FEL: $*" >&2; exit 1; }
say "Startar i läge: $MODE (repo: $ROOT)"
# ---------------------------------------------------------------------------
# GRIND 0: Förutsättningar
# ---------------------------------------------------------------------------
# Docker är VALFRITT: det används bara som bekvämlighet för att starta
# Postgres/Redis i staging när de inte redan svarar. Kör tjänsterna nativt
# (WSL, systemd, molndatabas) behövs ingen Docker alls Grind 2 avgör på
# faktisk nåbarhet, inte på verktygslistan.
if command -v docker >/dev/null && docker compose version >/dev/null 2>&1; then
DOCKER_OK=1
DOCKER_NOTE="docker+compose"
else
DOCKER_OK=0
DOCKER_NOTE="utan docker (ok om Postgres/Redis redan kör)"
say "OBS: docker saknas ingen fara om Postgres/Redis redan svarar (nativt/WSL/moln). Grind 2 verifierar."
fi
command -v node >/dev/null || fail "node saknas kräver Node >= 22."
NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])")
[ "$NODE_MAJOR" -ge 22 ] || fail "Node $NODE_MAJOR är för gammal kräver >= 22."
command -v pnpm >/dev/null || fail "pnpm saknas kör: corepack enable && corepack prepare pnpm@10 --activate"
say "GRIND 0 OK: $DOCKER_NOTE, node $NODE_MAJOR, pnpm $(pnpm --version)"
# ---------------------------------------------------------------------------
# GRIND 1: Miljöfil
# ---------------------------------------------------------------------------
if [ ! -f .env ]; then
if [ "$MODE" = "production" ]; then
fail ".env saknas. I produktion skapas den manuellt från .env.example med riktiga värden (Secrets Manager). Skriptet genererar ALDRIG produktionshemligheter."
fi
say "Ingen .env skapar staging-miljö med genererade hemligheter."
cp .env.example .env
gen() { node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"; }
# Ersätt dev-platshållare med riktiga slumpvärden (även staging förtjänar riktiga hemligheter).
sed -i "s|^JWT_ACCESS_SECRET=.*|JWT_ACCESS_SECRET=$(gen)|" .env
sed -i "s|^JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$(gen)|" .env
sed -i "s|^ENTITLEMENT_SIGNING_SECRET=.*|ENTITLEMENT_SIGNING_SECRET=$(gen)|" .env
# AAMOS: riktiga värden via miljövariabler vid körning vinner över mock.
if [ -n "${AAMOS_API_URL:-}" ] && [ -n "${AAMOS_API_KEY:-}" ]; then
sed -i "s|^AAMOS_MODE=.*|AAMOS_MODE=http|" .env
sed -i "s|^AAMOS_API_URL=.*|AAMOS_API_URL=$AAMOS_API_URL|" .env
sed -i "s|^AAMOS_API_KEY=.*|AAMOS_API_KEY=$AAMOS_API_KEY|" .env
say "AAMOS: riktiga uppgifter angivna kör http-läge från start."
else
sed -i "s|^AAMOS_MODE=.*|AAMOS_MODE=mock|" .env
fi
# E-post: riktig SMTP från start om uppgifter angivits.
if [ -n "${SMTP_HOST:-}" ] && [ -n "${MAIL_FROM:-}" ]; then
sed -i "s|^EMAIL_MODE=.*|EMAIL_MODE=smtp|" .env
sed -i "s|^SMTP_HOST=.*|SMTP_HOST=$SMTP_HOST|" .env
sed -i "s|^SMTP_PORT=.*|SMTP_PORT=${SMTP_PORT:-587}|" .env
sed -i "s|^SMTP_USER=.*|SMTP_USER=${SMTP_USER:-}|" .env
sed -i "s|^SMTP_PASS=.*|SMTP_PASS=${SMTP_PASS:-}|" .env
sed -i "s|^MAIL_FROM=.*|MAIL_FROM=$MAIL_FROM|" .env
say "E-post: SMTP-uppgifter angivna riktiga mejl från start."
fi
# Fillagring: riktig S3 från start om bucket angivits.
if [ -n "${S3_BUCKET:-}" ]; then
sed -i "s|^S3_MODE=.*|S3_MODE=aws|" .env
sed -i "s|^S3_BUCKET=.*|S3_BUCKET=$S3_BUCKET|" .env
[ -n "${S3_REGION:-}" ] && sed -i "s|^S3_REGION=.*|S3_REGION=$S3_REGION|" .env
[ -n "${S3_ENDPOINT:-}" ] && sed -i "s|^S3_ENDPOINT=.*|S3_ENDPOINT=$S3_ENDPOINT|" .env
[ -n "${S3_ACCESS_KEY_ID:-}" ] && sed -i "s|^S3_ACCESS_KEY_ID=.*|S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID|" .env
[ -n "${S3_SECRET_ACCESS_KEY:-}" ] && sed -i "s|^S3_SECRET_ACCESS_KEY=.*|S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY|" .env
say "Fillagring: S3-uppgifter angivna riktig lagring från start."
fi
chmod 600 .env
fi
get_env() { { grep -E "^$1=" .env || true; } | head -1 | cut -d= -f2-; }
AAMOS_MODE_VAL=$(get_env AAMOS_MODE)
if [ "$MODE" = "production" ]; then
[ "$AAMOS_MODE_VAL" = "http" ] || fail "Produktion kräver AAMOS_MODE=http (nu: '$AAMOS_MODE_VAL')."
[ -n "$(get_env AAMOS_API_URL)" ] || fail "AAMOS_API_URL saknas i .env."
[ -n "$(get_env AAMOS_API_KEY)" ] || fail "AAMOS_API_KEY saknas i .env."
for s in JWT_ACCESS_SECRET JWT_REFRESH_SECRET ENTITLEMENT_SIGNING_SECRET; do
case "$(get_env $s)" in dev-only-*|"") fail "$s har dev-/tomt värde sätt riktig hemlighet." ;; esac
done
fi
say "GRIND 1 OK: .env på plats ($([ "$MODE" = production ] && echo 'produktionsvärden verifierade' || echo 'staging'))"
# ---------------------------------------------------------------------------
# GRIND 2: Databas + Redis (nåbarhet avgör Docker är bara reservstart)
# ---------------------------------------------------------------------------
db_reachable() {
node -e "
const { Client } = require('pg');
const c = new Client({ connectionString: process.env.DB_URL, connectionTimeoutMillis: 3000 });
c.connect().then(() => c.end()).then(() => process.exit(0)).catch(() => process.exit(1));
" 2>/dev/null
}
# Rå TCP-PING: alla svar (även -NOAUTH) bevisar att Redis är nåbar.
redis_reachable() {
node -e "
const net = require('net');
let u; try { u = new URL(process.env.R_URL); } catch { process.exit(1); }
const s = net.createConnection({ host: u.hostname || '127.0.0.1', port: Number(u.port) || 6379 });
s.setTimeout(3000);
s.on('connect', () => s.write('PING\r\n'));
s.on('data', () => process.exit(0));
s.on('error', () => process.exit(1));
s.on('timeout', () => process.exit(1));
" 2>/dev/null
}
start_docker_services() {
# Reservstart i staging kräver Docker. Utan Docker: starta tjänsterna själv.
if [ "$DOCKER_OK" != 1 ] || ! docker info >/dev/null 2>&1; then
fail "$1 svarar inte och Docker är inte tillgänglig. Starta tjänsten nativt (t.ex. 'sudo service postgresql start' / 'sudo service redis-server start' på WSL) eller installera Docker och kör om skriptet."
fi
say "Startar Postgres + Redis i Docker (idempotent) …"
docker compose -f infrastructure/docker/docker-compose.dev.yml up -d
}
export DB_URL="$(get_env DATABASE_URL)"
export R_URL="$(get_env REDIS_URL)"
if DB_URL="$DB_URL" db_reachable; then
say "Databasen svarar redan ingen uppstart behövs."
elif [ "$MODE" = "staging" ]; then
start_docker_services "Databasen"
for i in $(seq 1 30); do
DB_URL="$DB_URL" db_reachable && break
sleep 1
[ "$i" = 30 ] && fail "Postgres svarade inte inom 30 s."
done
else
fail "DATABASE_URL svarar inte i produktion startar detta skript aldrig databaser. Kontrollera RDS/nätverk."
fi
if R_URL="$R_URL" redis_reachable; then
say "Redis svarar."
elif [ "$MODE" = "staging" ]; then
start_docker_services "Redis"
for i in $(seq 1 30); do
R_URL="$R_URL" redis_reachable && break
sleep 1
[ "$i" = 30 ] && fail "Redis svarade inte inom 30 s."
done
else
fail "REDIS_URL svarar inte i produktion startar detta skript aldrig tjänster. Kontrollera Redis/nätverk."
fi
say "GRIND 2 OK: databas + Redis nåbara"
# ---------------------------------------------------------------------------
# GRIND 3: Beroenden + verifiering av källkod
# ---------------------------------------------------------------------------
say "pnpm install (frozen lockfile) …"
pnpm install --frozen-lockfile >/dev/null
say "Varumärkesvakt …"
./scripts/brand-guard.sh
say "Typecheck + tester + build …"
pnpm typecheck >/dev/null
pnpm test >/dev/null
pnpm build >/dev/null
say "GRIND 3 OK: typecheck, tester, build, varumärkesvakt"
# ---------------------------------------------------------------------------
# GRIND 4: Migrationer (produktion: med backup före spec §63)
# ---------------------------------------------------------------------------
if [ "$MODE" = "production" ]; then
command -v pg_dump >/dev/null || fail "pg_dump saknas krävs för backup före migration."
mkdir -p "$ROOT/backups"
DBURL=$(get_env DATABASE_URL)
say "Backup före migration …"
pg_dump "$DBURL" --format=custom --file="$ROOT/backups/pre-first-deploy-$(date +%Y%m%d_%H%M%S).dump" \
|| fail "Backup misslyckades ingen migration utan backup (spec §63)."
fi
say "Kör migrationer …"
pnpm db:migrate
if [ "$MODE" = "staging" ]; then
say "Seedar databasen (endast staging) …"
pnpm db:seed
fi
say "GRIND 4 OK: databas migrerad$([ "$MODE" = staging ] && echo ' + seedad')"
# ---------------------------------------------------------------------------
# GRIND 5: AAMOS-utvärdering (spec §40)
# ---------------------------------------------------------------------------
say "Kör AAMOS-utvärderingssviten mot läge: $AAMOS_MODE_VAL"
pnpm eval:aamos || fail "Utvärderingssviten UNDERKÄND deploya inte (spec §40)."
say "GRIND 5 OK: eval-sviten GODKÄND"
# ---------------------------------------------------------------------------
# GRIND 6: Starta tjänsterna + hälsokontroll
# ---------------------------------------------------------------------------
API_PORT=$(get_env API_PORT); API_PORT=${API_PORT:-4000}
say "Startar API + worker (bakgrund) …"
# Idempotent omstart: döda ev. tidigare instanser från detta skript.
pkill -f "apps/api/dist/index.js" 2>/dev/null || true
pkill -f "apps/worker/dist/index.js" 2>/dev/null || true
sleep 1
mkdir -p "$ROOT/logs"
nohup node apps/api/dist/index.js >> "$ROOT/logs/api.log" 2>&1 &
nohup node apps/worker/dist/index.js >> "$ROOT/logs/worker.log" 2>&1 &
for i in $(seq 1 20); do
curl -fsS "http://127.0.0.1:$API_PORT/healthz" >/dev/null 2>&1 && break
sleep 1
[ "$i" = 20 ] && { tail -20 "$ROOT/logs/api.log"; fail "API svarade inte på /healthz inom 20 s."; }
done
curl -fsS "http://127.0.0.1:$API_PORT/healthz"
echo
curl -fsS "http://127.0.0.1:$API_PORT/readyz" >/dev/null || fail "/readyz underkänd DB/Redis-koppling trasig."
grep -q "Worker igång" "$ROOT/logs/worker.log" || sleep 3
grep -q "Worker igång" "$ROOT/logs/worker.log" || fail "Workern startade inte (se logs/worker.log)."
say "GRIND 6 OK: API + worker uppe, healthz/readyz gröna"
# ---------------------------------------------------------------------------
# GRIND 7: Funktionsrök (registrera -> logga in -> läsa recept)
# ---------------------------------------------------------------------------
SMOKE_EMAIL="smoke-$(date +%s)@example.com"
B="http://127.0.0.1:$API_PORT"
TOKEN=$(curl -fsS -X POST "$B/v1/auth/register" -H 'content-type: application/json' \
-d "{\"email\":\"$SMOKE_EMAIL\",\"password\":\"FirstDeploy123!\",\"displayName\":\"Deploy Smoke\",\"locale\":\"sv-SE\"}" \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).accessToken))")
[ -n "$TOKEN" ] || fail "Registrering i röktestet misslyckades."
RECIPES=$(curl -fsS "$B/v1/recipes?limit=1" -H "Authorization: Bearer $TOKEN" \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).recipes.length))")
if [ "$MODE" = "staging" ]; then
[ "$RECIPES" -ge 1 ] || fail "Röktestet hittade inga recept (seed saknas?)."
fi
say "GRIND 7 OK: register -> login -> recept fungerar"
# ---------------------------------------------------------------------------
# GRIND 8: Integrationsstatus vad är PÅ RIKTIGT och vad är mock?
# ---------------------------------------------------------------------------
EMAIL_MODE_VAL=$(get_env EMAIL_MODE); EMAIL_MODE_VAL=${EMAIL_MODE_VAL:-log}
S3_MODE_VAL=$(get_env S3_MODE); S3_MODE_VAL=${S3_MODE_VAL:-mock}
aamos_status=$([ "$AAMOS_MODE_VAL" = "http" ] && echo "RIKTIG (http)" || echo "mock")
email_status=$([ "$EMAIL_MODE_VAL" = "smtp" ] && echo "RIKTIG (smtp)" || echo "log (mejl till logs/api.log)")
s3_status=$([ "$S3_MODE_VAL" = "aws" ] && echo "RIKTIG (S3: $(get_env S3_BUCKET))" || echo "mock (lokal disk)")
say "-----------------------------------------------"
say "INTEGRATIONSSTATUS:"
say " AAMOS (AI): $aamos_status"
say " E-post: $email_status"
say " Fillagring: $s3_status"
say "-----------------------------------------------"
if [ "${REQUIRE_REAL:-0}" = "1" ]; then
MOCKS=""
[ "$AAMOS_MODE_VAL" != "http" ] && MOCKS="$MOCKS AAMOS(AAMOS_MODE=http+URL+nyckel)"
[ "$EMAIL_MODE_VAL" != "smtp" ] && MOCKS="$MOCKS E-post(EMAIL_MODE=smtp+SMTP_*+MAIL_FROM)"
[ "$S3_MODE_VAL" != "aws" ] && MOCKS="$MOCKS Fillagring(S3_MODE=aws+S3_*)"
if [ -n "$MOCKS" ]; then
fail "REQUIRE_REAL=1 men följande kör fortfarande mock:$MOCKS fyll i .env och kör om."
fi
say "GRIND 8 OK: REQUIRE_REAL uppfyllt alla integrationer är riktiga."
fi
# ---------------------------------------------------------------------------
# Klart
# ---------------------------------------------------------------------------
say "==============================================="
say "ALLA GRINDAR GRÖNA $MODE är uppe."
say "API: http://127.0.0.1:$API_PORT (Swagger: /docs)"
say "Loggar: $ROOT/logs/api.log, $ROOT/logs/worker.log"
if [ "$MODE" = "staging" ]; then
say "Admin: bygg finns i apps/admin/dist servera med valfri statisk server."
say "OBS: staging kör AAMOS_MODE=$AAMOS_MODE_VAL."
else
say "NÄSTA: peka reverse proxy (TLS) mot 127.0.0.1:$API_PORT och verifiera utifrån."
fi
say "==============================================="