From 759487cb36cc4a28ad5d99aff362b4c364f16bc1 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sun, 17 May 2026 10:19:16 +0000 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20ex=C3=A9cuter=20sur=20le=20serve?= =?UTF-8?q?ur=20+=20build=20CI=20r=C3=A9utilis=C3=A9=20(~2=20min)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le deploy SSH depuis ubuntu-24.04 ne rebuildait pas sur 190 (image d'hier). Déploiement sur runner docker-host, artifact Next.js de la CI, Dockerfile.prebuilt, script deploy-prod.sh. workflow_run supprimé au profit de needs:[ci] même fichier. Co-authored-by: Cursor --- .gitea/workflows/ci.yaml | 119 +++++++++++++++ .gitea/workflows/deploy.yaml | 251 +++++++------------------------ docker-compose.yml | 2 +- memento-note/Dockerfile.prebuilt | 36 +++++ scripts/deploy-prod.sh | 80 ++++++++++ 5 files changed, 291 insertions(+), 197 deletions(-) create mode 100644 memento-note/Dockerfile.prebuilt create mode 100755 scripts/deploy-prod.sh diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 275c96d..784bc18 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -68,3 +68,122 @@ jobs: - name: Build run: npm run build + + - name: Pack web artifact for deploy + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + working-directory: memento-note + run: | + tar czf ../web-artifact.tgz \ + .next/standalone .next/static public prisma \ + node_modules/.prisma node_modules/@prisma node_modules/prisma \ + docker-entrypoint.sh + + - name: Upload web artifact + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + working-directory: ${{ github.workspace }} + uses: actions/upload-artifact@v4 + with: + name: web-artifact + path: web-artifact.tgz + retention-days: 2 + + deploy: + name: Deploy production (on server) + needs: [ci] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + runs-on: docker-host + steps: + - name: Sync deploy scripts on server + run: | + cd /opt/memento + git fetch origin main + git reset --hard origin/main + + - name: Download web artifact + uses: actions/download-artifact@v4 + with: + name: web-artifact + + - name: Update .env.docker + env: + APP_URL: ${{ vars.APP_URL }} + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} + ADMIN_EMAIL: ${{ vars.ADMIN_EMAIL }} + ALLOW_REGISTRATION: ${{ vars.ALLOW_REGISTRATION }} + POSTGRES_USER: ${{ vars.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_DB: ${{ vars.POSTGRES_DB }} + POSTGRES_PORT: ${{ vars.POSTGRES_PORT }} + AI_PROVIDER_TAGS: ${{ vars.AI_PROVIDER_TAGS }} + AI_MODEL_TAGS: ${{ vars.AI_MODEL_TAGS }} + AI_PROVIDER_EMBEDDING: ${{ vars.AI_PROVIDER_EMBEDDING }} + AI_MODEL_EMBEDDING: ${{ vars.AI_MODEL_EMBEDDING }} + AI_PROVIDER_CHAT: ${{ vars.AI_PROVIDER_CHAT }} + AI_MODEL_CHAT: ${{ vars.AI_MODEL_CHAT }} + CUSTOM_OPENAI_BASE_URL: ${{ vars.CUSTOM_OPENAI_BASE_URL }} + CUSTOM_OPENAI_API_KEY: ${{ secrets.CUSTOM_OPENAI_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OLLAMA_BASE_URL: ${{ vars.OLLAMA_BASE_URL }} + EMAIL_PROVIDER: ${{ vars.EMAIL_PROVIDER }} + SMTP_FROM: ${{ vars.SMTP_FROM }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + SMTP_HOST: ${{ vars.SMTP_HOST }} + SMTP_PORT: ${{ vars.SMTP_PORT }} + SMTP_USER: ${{ vars.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + SMTP_SECURE: ${{ vars.SMTP_SECURE }} + SMTP_IGNORE_CERT: ${{ vars.SMTP_IGNORE_CERT }} + MCP_MODE: ${{ vars.MCP_MODE }} + MCP_PORT: ${{ vars.MCP_PORT }} + WEB_SEARCH_PROVIDER: ${{ vars.WEB_SEARCH_PROVIDER }} + SEARXNG_URL: ${{ vars.SEARXNG_URL }} + BRAVE_SEARCH_API_KEY: ${{ secrets.BRAVE_SEARCH_API_KEY }} + JINA_API_KEY: ${{ secrets.JINA_API_KEY }} + run: | + ENV_FILE="/opt/memento/.env.docker" + touch "$ENV_FILE" + upsert() { + local key="$1" val="$2" + [ -z "$val" ] && return + sed -i "/^[[:space:]]*${key}=/d" "$ENV_FILE" + echo "${key}=\"${val}\"" >> "$ENV_FILE" + } + upsert NEXTAUTH_URL "$APP_URL" + upsert NEXTAUTH_SECRET "$NEXTAUTH_SECRET" + upsert ADMIN_EMAIL "$ADMIN_EMAIL" + upsert ALLOW_REGISTRATION "$ALLOW_REGISTRATION" + upsert POSTGRES_USER "$POSTGRES_USER" + upsert POSTGRES_PASSWORD "$POSTGRES_PASSWORD" + upsert POSTGRES_DB "$POSTGRES_DB" + upsert POSTGRES_PORT "$POSTGRES_PORT" + upsert AI_PROVIDER_TAGS "$AI_PROVIDER_TAGS" + upsert AI_MODEL_TAGS "$AI_MODEL_TAGS" + upsert AI_PROVIDER_EMBEDDING "$AI_PROVIDER_EMBEDDING" + upsert AI_MODEL_EMBEDDING "$AI_MODEL_EMBEDDING" + upsert AI_PROVIDER_CHAT "$AI_PROVIDER_CHAT" + upsert AI_MODEL_CHAT "$AI_MODEL_CHAT" + upsert CUSTOM_OPENAI_BASE_URL "$CUSTOM_OPENAI_BASE_URL" + upsert CUSTOM_OPENAI_API_KEY "$CUSTOM_OPENAI_API_KEY" + upsert OPENAI_API_KEY "$OPENAI_API_KEY" + upsert OLLAMA_BASE_URL "$OLLAMA_BASE_URL" + upsert EMAIL_PROVIDER "$EMAIL_PROVIDER" + upsert SMTP_FROM "$SMTP_FROM" + upsert RESEND_API_KEY "$RESEND_API_KEY" + upsert SMTP_HOST "$SMTP_HOST" + upsert SMTP_PORT "$SMTP_PORT" + upsert SMTP_USER "$SMTP_USER" + upsert SMTP_PASS "$SMTP_PASS" + upsert SMTP_SECURE "$SMTP_SECURE" + upsert SMTP_IGNORE_CERT "$SMTP_IGNORE_CERT" + upsert MCP_MODE "$MCP_MODE" + upsert MCP_PORT "$MCP_PORT" + upsert WEB_SEARCH_PROVIDER "$WEB_SEARCH_PROVIDER" + upsert SEARXNG_URL "$SEARXNG_URL" + upsert BRAVE_SEARCH_API_KEY "$BRAVE_SEARCH_API_KEY" + upsert JINA_API_KEY "$JINA_API_KEY" + + - name: Deploy on 192.168.1.190 + env: + ARTIFACT_TGZ: ${{ github.workspace }}/web-artifact.tgz + EXPECTED_COMMIT: ${{ github.sha }} + run: bash /opt/memento/scripts/deploy-prod.sh diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 546407c..2306147 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -1,37 +1,23 @@ -name: Deploy to Production +name: Deploy to Production (manual) + +# Déploiement auto = job "deploy" dans ci.yaml (après CI, sur runner docker-host). +# Ce workflow sert uniquement au déclenchement manuel sans repasser par la CI. -# Deploy only after CI succeeds (needs: [ci] does NOT work across workflow files). on: - workflow_run: - workflows: ["CI"] - types: [completed] - branches: [main] workflow_dispatch: jobs: deploy: - name: Build and Deploy - runs-on: ubuntu-24.04 - if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + name: Deploy production (manual) + runs-on: docker-host steps: - - name: Resolve target commit - id: target + - name: Sync repo on server run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - SHA="${{ github.sha }}" - else - SHA="${{ github.event.workflow_run.head_sha }}" - fi - echo "sha=$SHA" >> "$GITHUB_OUTPUT" - echo "Deploying commit: $SHA" - - name: Setup SSH - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H 192.168.1.190 >> ~/.ssh/known_hosts + cd /opt/memento + git fetch origin main + git reset --hard origin/main - - name: Update .env.docker from Gitea vars & secrets + - name: Update .env.docker env: APP_URL: ${{ vars.APP_URL }} NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} @@ -67,176 +53,49 @@ jobs: BRAVE_SEARCH_API_KEY: ${{ secrets.BRAVE_SEARCH_API_KEY }} JINA_API_KEY: ${{ secrets.JINA_API_KEY }} run: | - ssh root@192.168.1.190 bash << 'ENDSSH' - ENV_FILE="/opt/memento/.env.docker" - touch "$ENV_FILE" + ENV_FILE="/opt/memento/.env.docker" + touch "$ENV_FILE" + upsert() { + local key="$1" val="$2" + [ -z "$val" ] && return + sed -i "/^[[:space:]]*${key}=/d" "$ENV_FILE" + echo "${key}=\"${val}\"" >> "$ENV_FILE" + } + upsert NEXTAUTH_URL "$APP_URL" + upsert NEXTAUTH_SECRET "$NEXTAUTH_SECRET" + upsert ADMIN_EMAIL "$ADMIN_EMAIL" + upsert ALLOW_REGISTRATION "$ALLOW_REGISTRATION" + upsert POSTGRES_USER "$POSTGRES_USER" + upsert POSTGRES_PASSWORD "$POSTGRES_PASSWORD" + upsert POSTGRES_DB "$POSTGRES_DB" + upsert POSTGRES_PORT "$POSTGRES_PORT" + upsert AI_PROVIDER_TAGS "$AI_PROVIDER_TAGS" + upsert AI_MODEL_TAGS "$AI_MODEL_TAGS" + upsert AI_PROVIDER_EMBEDDING "$AI_PROVIDER_EMBEDDING" + upsert AI_MODEL_EMBEDDING "$AI_MODEL_EMBEDDING" + upsert AI_PROVIDER_CHAT "$AI_PROVIDER_CHAT" + upsert AI_MODEL_CHAT "$AI_MODEL_CHAT" + upsert CUSTOM_OPENAI_BASE_URL "$CUSTOM_OPENAI_BASE_URL" + upsert CUSTOM_OPENAI_API_KEY "$CUSTOM_OPENAI_API_KEY" + upsert OPENAI_API_KEY "$OPENAI_API_KEY" + upsert OLLAMA_BASE_URL "$OLLAMA_BASE_URL" + upsert EMAIL_PROVIDER "$EMAIL_PROVIDER" + upsert SMTP_FROM "$SMTP_FROM" + upsert RESEND_API_KEY "$RESEND_API_KEY" + upsert SMTP_HOST "$SMTP_HOST" + upsert SMTP_PORT "$SMTP_PORT" + upsert SMTP_USER "$SMTP_USER" + upsert SMTP_PASS "$SMTP_PASS" + upsert SMTP_SECURE "$SMTP_SECURE" + upsert SMTP_IGNORE_CERT "$SMTP_IGNORE_CERT" + upsert MCP_MODE "$MCP_MODE" + upsert MCP_PORT "$MCP_PORT" + upsert WEB_SEARCH_PROVIDER "$WEB_SEARCH_PROVIDER" + upsert SEARXNG_URL "$SEARXNG_URL" + upsert BRAVE_SEARCH_API_KEY "$BRAVE_SEARCH_API_KEY" + upsert JINA_API_KEY "$JINA_API_KEY" - upsert() { - local key="$1" val="$2" - [ -z "$val" ] && return - sed -i "/^[[:space:]]*${key}=/d" "$ENV_FILE" - echo "${key}=\"${val}\"" >> "$ENV_FILE" - } - - upsert NEXTAUTH_URL "$APP_URL" - upsert NEXTAUTH_SECRET "$NEXTAUTH_SECRET" - upsert ADMIN_EMAIL "$ADMIN_EMAIL" - upsert ALLOW_REGISTRATION "$ALLOW_REGISTRATION" - upsert POSTGRES_USER "$POSTGRES_USER" - upsert POSTGRES_PASSWORD "$POSTGRES_PASSWORD" - upsert POSTGRES_DB "$POSTGRES_DB" - upsert POSTGRES_PORT "$POSTGRES_PORT" - upsert AI_PROVIDER_TAGS "$AI_PROVIDER_TAGS" - upsert AI_MODEL_TAGS "$AI_MODEL_TAGS" - upsert AI_PROVIDER_EMBEDDING "$AI_PROVIDER_EMBEDDING" - upsert AI_MODEL_EMBEDDING "$AI_MODEL_EMBEDDING" - upsert AI_PROVIDER_CHAT "$AI_PROVIDER_CHAT" - upsert AI_MODEL_CHAT "$AI_MODEL_CHAT" - upsert CUSTOM_OPENAI_BASE_URL "$CUSTOM_OPENAI_BASE_URL" - upsert CUSTOM_OPENAI_API_KEY "$CUSTOM_OPENAI_API_KEY" - upsert OPENAI_API_KEY "$OPENAI_API_KEY" - upsert OLLAMA_BASE_URL "$OLLAMA_BASE_URL" - upsert EMAIL_PROVIDER "$EMAIL_PROVIDER" - upsert SMTP_FROM "$SMTP_FROM" - upsert RESEND_API_KEY "$RESEND_API_KEY" - upsert SMTP_HOST "$SMTP_HOST" - upsert SMTP_PORT "$SMTP_PORT" - upsert SMTP_USER "$SMTP_USER" - upsert SMTP_PASS "$SMTP_PASS" - upsert SMTP_SECURE "$SMTP_SECURE" - upsert SMTP_IGNORE_CERT "$SMTP_IGNORE_CERT" - upsert MCP_MODE "$MCP_MODE" - upsert MCP_PORT "$MCP_PORT" - upsert WEB_SEARCH_PROVIDER "$WEB_SEARCH_PROVIDER" - upsert SEARXNG_URL "$SEARXNG_URL" - upsert BRAVE_SEARCH_API_KEY "$BRAVE_SEARCH_API_KEY" - upsert JINA_API_KEY "$JINA_API_KEY" - - echo ".env.docker updated (container restart after build in deploy step)" - ENDSSH - - - name: Tag current image as rollback - run: | - ssh root@192.168.1.190 << 'ENDSSH' - docker tag memento-note_memento-note:latest memento-note_memento-note:rollback 2>/dev/null && echo "Rollback tag saved" || echo "No existing image to tag" - ENDSSH - - - name: Deploy via SSH - run: | - ssh root@192.168.1.190 << 'ENDSSH' - set -e - cd /opt/memento - - echo "=== Git pull ===" - git config --global --add safe.directory /opt/memento - git fetch origin main - git reset --hard origin/main - - echo "=== Ensure postgres is running (no recreate — prod data) ===" - docker compose up -d postgres - - echo "=== Waiting for postgres healthy ===" - for i in $(seq 1 30); do - if docker compose exec -T postgres pg_isready -U "${POSTGRES_USER:-memento}" >/dev/null 2>&1; then - echo "Postgres healthy after $((i * 2))s" - break - fi - if [ "$i" -eq 30 ]; then - echo "ERROR: Postgres not healthy after 60s" - docker compose logs postgres --tail=30 - exit 1 - fi - sleep 2 - done - - echo "=== Create vector extension ===" - docker compose exec -T postgres psql -U "${POSTGRES_USER:-memento}" -d "${POSTGRES_DB:-memento}" -c "CREATE EXTENSION IF NOT EXISTS vector;" - - echo "=== Dump database before migration ===" - DUMP_FILE="/opt/memento/backups/pre-migrate-$(date +%Y%m%d-%H%M%S).sql.gz" - mkdir -p /opt/memento/backups - docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-memento}" -d "${POSTGRES_DB:-memento}" --clean --if-exists | gzip > "$DUMP_FILE" - DUMP_SIZE=$(stat -c%s "$DUMP_FILE") - if [ "$DUMP_SIZE" -lt 1048576 ]; then - echo "ERROR: Dump is only $DUMP_SIZE bytes (< 1MB). Aborting deploy!" - exit 1 - fi - echo "Backup saved: $DUMP_FILE ($(( DUMP_SIZE / 1024 ))KB)" - - echo "=== Building app images ===" - export GIT_COMMIT="$(git rev-parse HEAD)" - echo "GIT_COMMIT=$GIT_COMMIT" - export COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 - docker compose build memento-note - docker compose build mcp-server - - echo "=== Starting app containers with new images ===" - docker compose up -d --remove-orphans --force-recreate memento-note mcp-server - docker compose ps - echo "=== Deployed commit (git) ===" - git log -1 --oneline - docker inspect memento-web --format 'Image: {{.Image}} Created: {{.Created}}' - - echo "=== Reload reverse proxy (if nginx on host) ===" - nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null || true - ENDSSH - - - name: Verify deployed commit on app (port 3000) - id: health-check + - name: Deploy (full build, no CI artifact) env: - EXPECTED_SHA: ${{ steps.target.outputs.sha }} - run: | - echo "Expected commit: $EXPECTED_SHA" - for i in $(seq 1 36); do - BODY=$(ssh root@192.168.1.190 "curl -sf --max-time 5 http://127.0.0.1:3000/api/build-info" 2>/dev/null || true) - ACTUAL=$(echo "$BODY" | jq -r '.commit // empty' 2>/dev/null || true) - echo " [$((i * 5))s] build-info commit=$ACTUAL" - if [ -n "$ACTUAL" ] && [ "$ACTUAL" = "$EXPECTED_SHA" ]; then - echo "Deploy verified: container serves commit $ACTUAL" - echo "healthy=true" >> $GITHUB_OUTPUT - exit 0 - fi - sleep 5 - done - echo "healthy=false" >> $GITHUB_OUTPUT - echo "ERROR: App on :3000 does not serve expected commit." - echo " expected=$EXPECTED_SHA" - echo " got=$ACTUAL" - ssh root@192.168.1.190 "cd /opt/memento && git log -1 --oneline && docker logs memento-web --tail=40" - exit 0 - - - name: Rollback on failure - if: steps.health-check.outputs.healthy == 'false' - run: | - ssh root@192.168.1.190 << 'ENDSSH' - echo "=== ROLLBACK: Restoring previous image ===" - docker tag memento-note_memento-note:rollback memento-note_memento-note:latest - cd /opt/memento && docker compose up -d --force-recreate memento-note - echo "Rollback complete" - ENDSSH - - - name: Notify Telegram - if: always() - run: | - HEALTHY="${{ steps.health-check.outputs.healthy }}" - if [ "$HEALTHY" = "true" ]; then - MSG="✅ Memento deploy SUCCESS%nCommit: ${{ steps.target.outputs.sha }}" - else - MSG="❌ Memento deploy FAILED%nCommit: ${{ steps.target.outputs.sha }}%nAction: rollback to previous image" - fi - curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ - -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \ - -d text="$(printf "$MSG")" \ - -d parse_mode="HTML" || true - - - name: Fail if unhealthy - if: steps.health-check.outputs.healthy == 'false' - run: exit 1 - - - name: Cleanup - if: always() - run: | - ssh root@192.168.1.190 << 'ENDSSH' - docker image prune -f - cd /opt/memento/backups 2>/dev/null && ls -t pre-migrate-*.sql.gz | tail -n +11 | xargs -r rm -f && echo "Old backups cleaned" || true - ENDSSH + EXPECTED_COMMIT: ${{ github.sha }} + run: bash /opt/memento/scripts/deploy-prod.sh diff --git a/docker-compose.yml b/docker-compose.yml index 5a00923..3bedfd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: memento-note: build: context: ./memento-note - dockerfile: Dockerfile + dockerfile: ${MEMENTO_DOCKERFILE:-Dockerfile} args: GIT_COMMIT: ${GIT_COMMIT:-unknown} container_name: memento-web diff --git a/memento-note/Dockerfile.prebuilt b/memento-note/Dockerfile.prebuilt new file mode 100644 index 0000000..35aa5c2 --- /dev/null +++ b/memento-note/Dockerfile.prebuilt @@ -0,0 +1,36 @@ +# Image rapide : le build Next.js est déjà fait en CI (artifact). +FROM node:22-bookworm-slim AS runner +ARG GIT_COMMIT=unknown +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV GIT_COMMIT=$GIT_COMMIT + +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssl \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs + +COPY public ./public +RUN mkdir -p ./data/uploads/notes ./data/backups && chown -R nextjs:nodejs ./data + +COPY .next/standalone ./ +COPY .next/static ./.next/static + +COPY prisma ./prisma +COPY node_modules/.prisma ./node_modules/.prisma +COPY node_modules/@prisma ./node_modules/@prisma +COPY node_modules/prisma ./node_modules/prisma + +COPY docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x ./docker-entrypoint.sh + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh new file mode 100755 index 0000000..16ae723 --- /dev/null +++ b/scripts/deploy-prod.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Déploiement prod sur 192.168.1.190 — exécuter SUR LE SERVEUR (runner docker-host). +set -euo pipefail + +ROOT="${DEPLOY_ROOT:-/opt/memento}" +ARTIFACT_TGZ="${ARTIFACT_TGZ:-}" +EXPECTED_COMMIT="${EXPECTED_COMMIT:-}" + +cd "$ROOT" +git config --global --add safe.directory "$ROOT" 2>/dev/null || true +git fetch origin main +git reset --hard origin/main +export GIT_COMMIT="$(git rev-parse HEAD)" +echo "=== Deploy $GIT_COMMIT ===" + +if [ -n "$EXPECTED_COMMIT" ] && [ "$GIT_COMMIT" != "$EXPECTED_COMMIT" ]; then + echo "ERROR: git HEAD $GIT_COMMIT != expected $EXPECTED_COMMIT" + exit 1 +fi + +docker compose up -d postgres +for i in $(seq 1 30); do + docker compose exec -T postgres pg_isready -U "${POSTGRES_USER:-memento}" >/dev/null 2>&1 && break + [ "$i" -eq 30 ] && { echo "Postgres not ready"; exit 1; } + sleep 2 +done + +docker compose exec -T postgres psql -U "${POSTGRES_USER:-memento}" -d "${POSTGRES_DB:-memento}" -c "CREATE EXTENSION IF NOT EXISTS vector;" >/dev/null + +if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^memento-note/prisma/migrations/'; then + DUMP_FILE="/opt/memento/backups/pre-migrate-$(date +%Y%m%d-%H%M%S).sql.gz" + mkdir -p /opt/memento/backups + docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-memento}" -d "${POSTGRES_DB:-memento}" --clean --if-exists | gzip > "$DUMP_FILE" + DUMP_SIZE=$(stat -c%s "$DUMP_FILE") + if [ "$DUMP_SIZE" -lt 1048576 ]; then + echo "ERROR: Dump too small ($DUMP_SIZE bytes)" + exit 1 + fi + echo "Backup: $DUMP_FILE ($(( DUMP_SIZE / 1024 ))KB)" +else + echo "No migration change — skip pre-migrate dump" +fi + +export COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 +export GIT_COMMIT + +if [ -n "$ARTIFACT_TGZ" ] && [ -f "$ARTIFACT_TGZ" ]; then + echo "=== Fast image (CI artifact) ===" + tar xzf "$ARTIFACT_TGZ" -C memento-note/ + export MEMENTO_DOCKERFILE=Dockerfile.prebuilt +else + echo "=== Full docker build (no artifact) ===" + export MEMENTO_DOCKERFILE=Dockerfile +fi + +docker compose build memento-note + +if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^mcp-server/'; then + docker compose build mcp-server +fi + +docker compose up -d --remove-orphans --force-recreate memento-note +docker compose up -d mcp-server 2>/dev/null || true + +nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null || true + +for i in $(seq 1 24); do + BODY=$(curl -sf --max-time 3 http://127.0.0.1:3000/api/build-info 2>/dev/null || true) + ACTUAL=$(echo "$BODY" | jq -r '.commit // empty' 2>/dev/null || true) + if [ "$ACTUAL" = "$GIT_COMMIT" ]; then + echo "OK build-info commit=$ACTUAL" + docker compose ps + exit 0 + fi + sleep 5 +done + +echo "ERROR: build-info=$ACTUAL expected=$GIT_COMMIT" +docker logs memento-web --tail=40 +exit 1