diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 50d579a9..c8b16db2 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -14,8 +14,8 @@ on: default: 'false' env: - PUPPETEER_SKIP_DOWNLOAD: "true" - COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com" + PUPPETEER_SKIP_DOWNLOAD: 'true' + COREPACK_NPM_REGISTRY: 'https://registry.npmmirror.com' concurrency: group: deploy-pipeline @@ -29,16 +29,16 @@ jobs: name: ๐Ÿ” Prepare runs-on: docker outputs: - target: ${{ steps.determine.outputs.target }} - image_tag: ${{ steps.determine.outputs.image_tag }} - env_file: ${{ steps.determine.outputs.env_file }} - traefik_host: ${{ steps.determine.outputs.traefik_host }} - traefik_rule: ${{ steps.determine.outputs.traefik_rule }} - next_public_url: ${{ steps.determine.outputs.next_public_url }} - project_name: ${{ steps.determine.outputs.project_name }} - short_sha: ${{ steps.determine.outputs.short_sha }} - slug: ${{ steps.determine.outputs.slug }} - gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }} + target: ${{ steps.determine.outputs.target }} + image_tag: ${{ steps.determine.outputs.image_tag }} + env_file: ${{ steps.determine.outputs.env_file }} + traefik_host: ${{ steps.determine.outputs.traefik_host }} + traefik_rule: ${{ steps.determine.outputs.traefik_rule }} + next_public_url: ${{ steps.determine.outputs.next_public_url }} + project_name: ${{ steps.determine.outputs.project_name }} + short_sha: ${{ steps.determine.outputs.short_sha }} + slug: ${{ steps.determine.outputs.slug }} + gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }} container: image: catthehacker/ubuntu:act-latest steps: @@ -96,7 +96,7 @@ jobs: TRAEFIK_RULE='Host(`'"$TRAEFIK_HOST"'`)' PRIMARY_HOST="$TRAEFIK_HOST" fi - + GATEKEEPER_HOST="gatekeeper.$PRIMARY_HOST" { @@ -187,10 +187,13 @@ jobs: - name: ๐Ÿ”’ Security Audit run: pnpm audit --audit-level high || echo "โš ๏ธ Audit found vulnerabilities (non-blocking)" + - name: ๐Ÿงน Clean Workspace + run: rm -rf .next .turbo || true + - name: ๐Ÿงช QA Checks if: github.event.inputs.skip_checks != 'true' env: - TURBO_TELEMETRY_DISABLED: "1" + TURBO_TELEMETRY_DISABLED: '1' run: npx turbo run lint typecheck test --cache-dir=".turbo" # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # JOB 3: Build & Push @@ -252,54 +255,54 @@ jobs: container: image: catthehacker/ubuntu:act-latest env: - TARGET: ${{ needs.prepare.outputs.target }} - IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} - PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} + TARGET: ${{ needs.prepare.outputs.target }} + IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} + PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} - TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} - GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }} - SLUG: ${{ needs.prepare.outputs.slug }} - + TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} + GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }} + SLUG: ${{ needs.prepare.outputs.slug }} + # Secrets mapping (Payload CMS) - PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }} - PAYLOAD_DB_NAME: ${{ secrets.PAYLOAD_DB_NAME || vars.PAYLOAD_DB_NAME || 'payload' }} - PAYLOAD_DB_USER: ${{ secrets.PAYLOAD_DB_USER || vars.PAYLOAD_DB_USER || 'payload' }} - PAYLOAD_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_DB_PASSWORD) || secrets.PAYLOAD_DB_PASSWORD || vars.PAYLOAD_DB_PASSWORD || 'payload' }} - + PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }} + PAYLOAD_DB_NAME: ${{ secrets.PAYLOAD_DB_NAME || vars.PAYLOAD_DB_NAME || 'payload' }} + PAYLOAD_DB_USER: ${{ secrets.PAYLOAD_DB_USER || vars.PAYLOAD_DB_USER || 'payload' }} + PAYLOAD_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_DB_PASSWORD) || secrets.PAYLOAD_DB_PASSWORD || vars.PAYLOAD_DB_PASSWORD || 'payload' }} + # Secrets mapping (Mail) - MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }} - MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }} - MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }} - MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }} - MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }} - MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }} - + MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }} + MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }} + MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }} + MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }} + MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }} + MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }} + # Monitoring - SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} - + SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} + # Gatekeeper GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} # Analytics - UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }} - UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} - + UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }} + UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} + # Search & AI - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || vars.OPENROUTER_API_KEY }} - QDRANT_URL: ${{ secrets.QDRANT_URL || vars.QDRANT_URL || 'http://klz-qdrant:6333' }} - QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY || vars.QDRANT_API_KEY }} - REDIS_URL: ${{ secrets.REDIS_URL || vars.REDIS_URL || 'redis://klz-redis:6379' }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || vars.OPENROUTER_API_KEY }} + QDRANT_URL: ${{ secrets.QDRANT_URL || vars.QDRANT_URL || 'http://klz-qdrant:6333' }} + QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY || vars.QDRANT_API_KEY }} + REDIS_URL: ${{ secrets.REDIS_URL || vars.REDIS_URL || 'redis://klz-redis:6379' }} # Container Registry (standalone) - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }} + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: ๐Ÿ“ Generate Environment shell: bash env: - TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }} - ENV_FILE: ${{ needs.prepare.outputs.env_file }} + TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }} + ENV_FILE: ${{ needs.prepare.outputs.env_file }} run: | # Middleware Selection Logic # Regular app routes get auth on non-production @@ -307,7 +310,7 @@ jobs: LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" ) COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||') STD_MW="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-compress" - + if [[ "$TARGET" == "production" ]]; then AUTH_MIDDLEWARE="$STD_MW" COMPOSE_PROFILES="" @@ -404,7 +407,7 @@ jobs: - name: ๐Ÿš€ SSH Deploy shell: bash env: - ENV_FILE: ${{ needs.prepare.outputs.env_file }} + ENV_FILE: ${{ needs.prepare.outputs.env_file }} run: | mkdir -p ~/.ssh echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519 @@ -428,10 +431,10 @@ jobs: ssh root@alpha.mintel.me "mkdir -p $SITE_DIR" scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml - + # Execute remote commands โ€” alpha is pre-logged into registry.infra.mintel.me ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans" - + # Sanitize Payload Migrations: Replace 'dev' push entries with proper migration names. # Without this, Payload prompts interactively for confirmation and blocks forever in Docker. DB_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-db-1" @@ -444,7 +447,7 @@ jobs: echo " Attempt $i/15..." sleep 2 done - + echo "๐Ÿ”ง Sanitizing payload_migrations table (if exists)..." REMOTE_DB_USER=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_USER=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload") REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload") @@ -455,7 +458,7 @@ jobs: # This ensures fresh branch deployments (empty DBs) get their schema on first deploy. echo "๐Ÿ”„ Running Payload migrations..." MIGRATOR_IMAGE="registry.infra.mintel.me/mintel/klz-2026:migrate-$IMAGE_TAG" - + ssh root@alpha.mintel.me " echo '${{ steps.auth.outputs.token }}' | docker login registry.infra.mintel.me -u '${{ steps.auth.outputs.user }}' --password-stdin 2>/dev/null || true docker pull $MIGRATOR_IMAGE @@ -466,7 +469,7 @@ jobs: && echo 'โœ… Migrations complete.' \ || echo 'โš ๏ธ Migrations failed or already up-to-date โ€” continuing.' " - + # Restart app to pick up clean migration state APP_CONTAINER="${PROJECT_NAME}-klz-app-1" ssh root@alpha.mintel.me "docker restart $APP_CONTAINER" @@ -518,7 +521,7 @@ jobs: with: path: /usr/bin/chromium key: ${{ runner.os }}-chromium-native-${{ hashFiles('package.json') }} - + - name: ๐Ÿ” Install Chromium (Native & ARM64) if: steps.cache-chromium.outputs.cache-hit != 'true' run: |