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