From 83d79da3aad340ff0d8758d0fb36529454f105c1 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:51:41 -0600 Subject: [PATCH] CI/CD Gitea v1.0 --- .gitea/workflows/maintenance.yaml | 67 ++++ .gitea/workflows/mirror-images.yaml | 38 +++ .gitea/workflows/production.yaml | 260 ++++++++++++++++ .gitea/workflows/staging.yaml | 234 ++++++++++++++ .gitlab-ci.yml | 436 -------------------------- backend/Dockerfile | 6 +- docker-compose.blue-green.yml | 8 +- docker-compose.staging.yml | 80 +++++ docker-compose.yml | 8 +- docs/BUILD-SERVER-SETUP.md | 335 +++++++++++--------- docs/CICD-DEPLOY.md | 466 +++++++++++----------------- frontend/Dockerfile | 6 +- scripts/ci/mirror-base-images.sh | 2 +- scripts/ci/notify.sh | 24 +- scripts/inject-secrets.sh | 60 ++-- 15 files changed, 1101 insertions(+), 929 deletions(-) create mode 100644 .gitea/workflows/maintenance.yaml create mode 100644 .gitea/workflows/mirror-images.yaml create mode 100644 .gitea/workflows/production.yaml create mode 100644 .gitea/workflows/staging.yaml delete mode 100644 .gitlab-ci.yml create mode 100644 docker-compose.staging.yml diff --git a/.gitea/workflows/maintenance.yaml b/.gitea/workflows/maintenance.yaml new file mode 100644 index 0000000..0d46644 --- /dev/null +++ b/.gitea/workflows/maintenance.yaml @@ -0,0 +1,67 @@ +# MotoVaultPro Maintenance Migration Workflow +# Manual trigger for breaking database migrations requiring downtime + +name: Maintenance Migration +run-name: Maintenance Migration + +on: + workflow_dispatch: + inputs: + create_backup: + description: 'Create database backup before migration' + required: true + default: 'yes' + type: choice + options: + - 'yes' + - 'no' + +env: + DEPLOY_PATH: /opt/motovaultpro + +jobs: + maintenance-migration: + name: Run Maintenance Migration + runs-on: mvp-prod + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Send maintenance start notification + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/notify.sh + ./scripts/ci/notify.sh maintenance_start "Starting maintenance window for database migration" + env: + DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + + - name: Run maintenance migration + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/maintenance-migrate.sh + if [ "${{ inputs.create_backup }}" = "yes" ]; then + ./scripts/ci/maintenance-migrate.sh backup + else + ./scripts/ci/maintenance-migrate.sh + fi + + - name: Send maintenance complete notification + if: success() + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/notify.sh + ./scripts/ci/notify.sh maintenance_end "Maintenance window complete. Database migration successful." + env: + DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + + - name: Send maintenance failure notification + if: failure() + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/notify.sh + ./scripts/ci/notify.sh failure "Maintenance migration FAILED. Manual intervention required." + env: + DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} diff --git a/.gitea/workflows/mirror-images.yaml b/.gitea/workflows/mirror-images.yaml new file mode 100644 index 0000000..f7362ee --- /dev/null +++ b/.gitea/workflows/mirror-images.yaml @@ -0,0 +1,38 @@ +# MotoVaultPro Base Image Mirroring Workflow +# Mirrors upstream Docker images to Gitea Package Registry +# Runs weekly on schedule or manual trigger + +name: Mirror Base Images +run-name: Mirror Base Images to Registry + +on: + schedule: + # Run every Sunday at 3:00 AM UTC + - cron: '0 3 * * 0' + workflow_dispatch: + +env: + REGISTRY: git.motovaultpro.com + +jobs: + mirror: + name: Mirror Base Images + runs-on: mvp-build + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to Gitea Container Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY" + + - name: Run mirror script + run: | + chmod +x scripts/ci/mirror-base-images.sh + REGISTRY=$REGISTRY/egullickson/mirrors ./scripts/ci/mirror-base-images.sh + + - name: Report results + if: always() + run: | + echo "Base image mirroring complete" + echo "Mirrored images available at: $REGISTRY/egullickson/mirrors/" diff --git a/.gitea/workflows/production.yaml b/.gitea/workflows/production.yaml new file mode 100644 index 0000000..22f6679 --- /dev/null +++ b/.gitea/workflows/production.yaml @@ -0,0 +1,260 @@ +# MotoVaultPro Production Deployment Workflow +# Manual trigger only - run after verifying staging +# Blue-green deployment with auto-rollback + +name: Deploy to Production +run-name: Production Deploy - ${{ inputs.image_tag || 'latest' }} + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to deploy (defaults to latest)' + required: false + default: 'latest' + +env: + REGISTRY: git.motovaultpro.com + DEPLOY_PATH: /opt/motovaultpro + COMPOSE_FILE: docker-compose.yml + COMPOSE_BLUE_GREEN: docker-compose.blue-green.yml + HEALTH_CHECK_TIMEOUT: "60" + +jobs: + # ============================================ + # VALIDATE - Determine target stack + # ============================================ + validate: + name: Validate Prerequisites + runs-on: mvp-prod + outputs: + target_stack: ${{ steps.determine-stack.outputs.target_stack }} + backend_image: ${{ steps.set-images.outputs.backend_image }} + frontend_image: ${{ steps.set-images.outputs.frontend_image }} + steps: + - name: Check Docker availability + run: | + docker info > /dev/null 2>&1 || (echo "ERROR - Docker not accessible" && exit 1) + docker compose version > /dev/null 2>&1 || (echo "ERROR - Docker Compose not available" && exit 1) + + - name: Check deployment path + run: test -d "$DEPLOY_PATH" || (echo "ERROR - DEPLOY_PATH not found" && exit 1) + + - name: Login to Gitea Container Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY" + + - name: Set image tags + id: set-images + run: | + TAG="${{ inputs.image_tag }}" + echo "backend_image=$REGISTRY/egullickson/backend:$TAG" >> $GITHUB_OUTPUT + echo "frontend_image=$REGISTRY/egullickson/frontend:$TAG" >> $GITHUB_OUTPUT + + - name: Determine target stack + id: determine-stack + run: | + STATE_FILE="$DEPLOY_PATH/config/deployment/state.json" + if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then + ACTIVE_STACK=$(jq -r '.active_stack // "blue"' "$STATE_FILE") + if [ "$ACTIVE_STACK" = "blue" ]; then + echo "target_stack=green" >> $GITHUB_OUTPUT + echo "Deploying to GREEN stack (BLUE is currently active)" + else + echo "target_stack=blue" >> $GITHUB_OUTPUT + echo "Deploying to BLUE stack (GREEN is currently active)" + fi + else + echo "target_stack=green" >> $GITHUB_OUTPUT + echo "No state file found, defaulting to GREEN stack" + fi + + # ============================================ + # DEPLOY PROD - Blue-green deployment + # ============================================ + deploy-prod: + name: Deploy to Production + runs-on: mvp-prod + needs: validate + env: + TARGET_STACK: ${{ needs.validate.outputs.target_stack }} + BACKEND_IMAGE: ${{ needs.validate.outputs.backend_image }} + FRONTEND_IMAGE: ${{ needs.validate.outputs.frontend_image }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY" + + - name: Inject secrets + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/inject-secrets.sh + ./scripts/inject-secrets.sh + env: + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} + AUTH0_MANAGEMENT_CLIENT_ID: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_ID }} + AUTH0_MANAGEMENT_CLIENT_SECRET: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_SECRET }} + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + GOOGLE_MAPS_MAP_ID: ${{ secrets.GOOGLE_MAPS_MAP_ID }} + CF_DNS_API_TOKEN: ${{ secrets.CF_DNS_API_TOKEN }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + + - name: Initialize data directories + run: | + cd "$DEPLOY_PATH" + sudo mkdir -p data/backups data/documents + sudo chown -R 1001:1001 data/backups data/documents + sudo chmod 755 data/backups data/documents + + - name: Pull new images + run: | + docker pull $BACKEND_IMAGE + docker pull $FRONTEND_IMAGE + + - name: Start target stack + run: | + cd "$DEPLOY_PATH" + export BACKEND_IMAGE=$BACKEND_IMAGE + export FRONTEND_IMAGE=$FRONTEND_IMAGE + docker compose -f $COMPOSE_FILE -f $COMPOSE_BLUE_GREEN up -d \ + mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK + + - name: Wait for stack initialization + run: sleep 10 + + - name: Run health check + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/health-check.sh + ./scripts/ci/health-check.sh $TARGET_STACK $HEALTH_CHECK_TIMEOUT + + - name: Switch traffic + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/switch-traffic.sh + ./scripts/ci/switch-traffic.sh $TARGET_STACK instant + + - name: Update deployment state + run: | + cd "$DEPLOY_PATH" + STATE_FILE="config/deployment/state.json" + if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then + TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + jq --arg commit "${{ inputs.image_tag }}" \ + --arg ts "$TIMESTAMP" \ + '.last_deployment = $ts | .last_deployment_commit = $commit | .last_deployment_status = "success" | .rollback_available = true' \ + "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + fi + + # ============================================ + # VERIFY PROD - External health check + # ============================================ + verify-prod: + name: Verify Production + runs-on: mvp-prod + needs: [validate, deploy-prod] + env: + TARGET_STACK: ${{ needs.validate.outputs.target_stack }} + steps: + - name: Wait for routing propagation + run: sleep 5 + + - name: External health check + run: | + for i in 1 2 3 4 5 6; do + if curl -sf https://motovaultpro.com/api/health > /dev/null 2>&1; then + echo "OK: Production external health check passed" + exit 0 + fi + if [ $i -eq 6 ]; then + echo "ERROR: Production external health check failed after 6 attempts" + exit 1 + fi + echo "Attempt $i/6: Waiting 10s..." + sleep 10 + done + + - name: Verify container status + run: | + for service in mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK; do + status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found") + health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown") + if [ "$status" != "running" ] || [ "$health" != "healthy" ]; then + echo "ERROR: $service is not healthy (status: $status, health: $health)" + docker logs $service --tail 50 2>/dev/null || true + exit 1 + fi + echo "OK: $service is running and healthy" + done + + # ============================================ + # ROLLBACK - Auto-rollback on failure + # ============================================ + rollback: + name: Auto Rollback + runs-on: mvp-prod + needs: [validate, deploy-prod, verify-prod] + if: failure() + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Execute rollback + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/auto-rollback.sh + ./scripts/ci/auto-rollback.sh "Production verification failed - automatic rollback" + + - name: Update state + run: | + cd "$DEPLOY_PATH" + STATE_FILE="config/deployment/state.json" + if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then + jq '.last_deployment_status = "rolled_back"' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + fi + + # ============================================ + # NOTIFY SUCCESS + # ============================================ + notify-success: + name: Notify Success + runs-on: mvp-prod + needs: [validate, verify-prod] + if: success() + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Send success notification + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/notify.sh + ./scripts/ci/notify.sh success "Production deployment successful - ${{ inputs.image_tag }} is now live" ${{ inputs.image_tag }} + env: + DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + + # ============================================ + # NOTIFY FAILURE + # ============================================ + notify-failure: + name: Notify Failure + runs-on: mvp-prod + needs: [validate, deploy-prod, verify-prod, rollback] + if: failure() + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Send failure notification + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/notify.sh + ./scripts/ci/notify.sh failure "Production deployment failed for ${{ inputs.image_tag }}" ${{ inputs.image_tag }} + env: + DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} diff --git a/.gitea/workflows/staging.yaml b/.gitea/workflows/staging.yaml new file mode 100644 index 0000000..8b628d7 --- /dev/null +++ b/.gitea/workflows/staging.yaml @@ -0,0 +1,234 @@ +# MotoVaultPro Staging Deployment Workflow +# Triggers on push to main, builds and deploys to staging.motovaultpro.com +# After verification, sends notification with link to trigger production deploy + +name: Deploy to Staging +run-name: Staging Deploy - ${{ gitea.sha }} + +on: + push: + branches: + - main + +env: + REGISTRY: git.motovaultpro.com + DEPLOY_PATH: /opt/motovaultpro + COMPOSE_FILE: docker-compose.yml + COMPOSE_STAGING: docker-compose.staging.yml + HEALTH_CHECK_TIMEOUT: "60" + +jobs: + # ============================================ + # BUILD - Build and push images + # ============================================ + build: + name: Build Images + runs-on: mvp-build + outputs: + backend_image: ${{ steps.tags.outputs.backend_image }} + frontend_image: ${{ steps.tags.outputs.frontend_image }} + short_sha: ${{ steps.tags.outputs.short_sha }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to Gitea Container Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY" + + - name: Set image tags + id: tags + run: | + SHORT_SHA="${{ gitea.sha }}" + SHORT_SHA="${SHORT_SHA:0:7}" + echo "backend_image=$REGISTRY/egullickson/backend:$SHORT_SHA" >> $GITHUB_OUTPUT + echo "frontend_image=$REGISTRY/egullickson/frontend:$SHORT_SHA" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + + - name: Build backend image + run: | + docker build \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --build-arg REGISTRY_MIRRORS=$REGISTRY/egullickson/mirrors \ + --cache-from $REGISTRY/egullickson/backend:latest \ + -t ${{ steps.tags.outputs.backend_image }} \ + -t $REGISTRY/egullickson/backend:latest \ + -f backend/Dockerfile \ + . + + - name: Build frontend image + run: | + docker build \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --build-arg REGISTRY_MIRRORS=$REGISTRY/egullickson/mirrors \ + --build-arg VITE_AUTH0_DOMAIN=${{ vars.VITE_AUTH0_DOMAIN }} \ + --build-arg VITE_AUTH0_CLIENT_ID=${{ vars.VITE_AUTH0_CLIENT_ID }} \ + --build-arg VITE_AUTH0_AUDIENCE=${{ vars.VITE_AUTH0_AUDIENCE }} \ + --build-arg VITE_API_BASE_URL=/api \ + --cache-from $REGISTRY/egullickson/frontend:latest \ + -t ${{ steps.tags.outputs.frontend_image }} \ + -t $REGISTRY/egullickson/frontend:latest \ + -f frontend/Dockerfile \ + frontend + + - name: Push images + run: | + docker push ${{ steps.tags.outputs.backend_image }} + docker push ${{ steps.tags.outputs.frontend_image }} + docker push $REGISTRY/egullickson/backend:latest + docker push $REGISTRY/egullickson/frontend:latest + + # ============================================ + # DEPLOY STAGING - Deploy to staging server + # ============================================ + deploy-staging: + name: Deploy to Staging + runs-on: mvp-build + needs: build + env: + BACKEND_IMAGE: ${{ needs.build.outputs.backend_image }} + FRONTEND_IMAGE: ${{ needs.build.outputs.frontend_image }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY" + + - name: Inject secrets + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/inject-secrets.sh + SECRETS_DIR="$DEPLOY_PATH/secrets/staging" ./scripts/inject-secrets.sh + env: + POSTGRES_PASSWORD: ${{ secrets.STAGING_POSTGRES_PASSWORD }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} + AUTH0_MANAGEMENT_CLIENT_ID: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_ID }} + AUTH0_MANAGEMENT_CLIENT_SECRET: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_SECRET }} + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + GOOGLE_MAPS_MAP_ID: ${{ secrets.GOOGLE_MAPS_MAP_ID }} + CF_DNS_API_TOKEN: ${{ secrets.CF_DNS_API_TOKEN }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + + - name: Initialize data directories + run: | + cd "$DEPLOY_PATH" + sudo mkdir -p data/backups data/documents + sudo chown -R 1001:1001 data/backups data/documents + sudo chmod 755 data/backups data/documents + + - name: Pull new images + run: | + docker pull $BACKEND_IMAGE + docker pull $FRONTEND_IMAGE + + - name: Deploy staging stack + run: | + cd "$DEPLOY_PATH" + export BACKEND_IMAGE=$BACKEND_IMAGE + export FRONTEND_IMAGE=$FRONTEND_IMAGE + docker compose -f $COMPOSE_FILE -f $COMPOSE_STAGING down --timeout 30 || true + docker compose -f $COMPOSE_FILE -f $COMPOSE_STAGING up -d + + - name: Wait for services + run: sleep 15 + + # ============================================ + # VERIFY STAGING - Health checks + # ============================================ + verify-staging: + name: Verify Staging + runs-on: mvp-build + needs: [build, deploy-staging] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check container status + run: | + for service in mvp-frontend-staging mvp-backend-staging mvp-postgres-staging mvp-redis-staging; do + status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found") + if [ "$status" != "running" ]; then + echo "ERROR: $service is not running (status: $status)" + docker logs $service --tail 50 2>/dev/null || true + exit 1 + fi + echo "OK: $service is running" + done + + - name: Wait for backend health + run: | + for i in 1 2 3 4 5 6; do + if docker exec mvp-backend-staging curl -sf http://localhost:3001/health > /dev/null 2>&1; then + echo "OK: Backend health check passed" + exit 0 + fi + if [ $i -eq 6 ]; then + echo "ERROR: Backend health check failed after 6 attempts" + docker logs mvp-backend-staging --tail 100 + exit 1 + fi + echo "Attempt $i/6: Backend not ready, waiting 10s..." + sleep 10 + done + + - name: Check external endpoint + run: | + for i in 1 2 3 4 5 6; do + if curl -sf https://staging.motovaultpro.com/api/health > /dev/null 2>&1; then + echo "OK: Staging external health check passed" + exit 0 + fi + if [ $i -eq 6 ]; then + echo "ERROR: Staging external health check failed after 6 attempts" + exit 1 + fi + echo "Attempt $i/6: Waiting 10s..." + sleep 10 + done + + # ============================================ + # NOTIFY - Staging ready for production + # ============================================ + notify-staging-ready: + name: Notify Staging Ready + runs-on: mvp-build + needs: [build, verify-staging] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Send staging ready notification + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/notify.sh + ./scripts/ci/notify.sh staging_ready \ + "Staging verified for commit ${{ needs.build.outputs.short_sha }}. Review at https://staging.motovaultpro.com then trigger production deploy at https://git.motovaultpro.com/egullickson/motovaultpro/actions" \ + ${{ needs.build.outputs.short_sha }} + env: + DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + + # ============================================ + # NOTIFY FAILURE - Staging failed + # ============================================ + notify-staging-failure: + name: Notify Staging Failure + runs-on: mvp-build + needs: [build, deploy-staging, verify-staging] + if: failure() + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Send failure notification + run: | + cd "$DEPLOY_PATH" + chmod +x scripts/ci/notify.sh + SHORT_SHA="${{ gitea.sha }}" + SHORT_SHA="${SHORT_SHA:0:7}" + ./scripts/ci/notify.sh failure "Staging deployment failed for commit $SHORT_SHA" $SHORT_SHA + env: + DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 202b688..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,436 +0,0 @@ -# MotoVaultPro GitLab CI/CD Pipeline - Blue-Green Deployment -# GitLab 18.6+ with separate build and production runners -# See docs/CICD-DEPLOY.md for complete documentation -# v2.0 - Blue-Green with Auto-Rollback - -stages: - - validate - - build - - deploy-prepare - - deploy-switch - - verify - - rollback - - notify - -variables: - # Registry configuration - REGISTRY: registry.motovaultpro.com - REGISTRY_MIRRORS: ${REGISTRY}/mirrors - IMAGE_TAG: ${CI_COMMIT_SHORT_SHA} - BACKEND_IMAGE: ${REGISTRY}/motovaultpro/backend:${IMAGE_TAG} - FRONTEND_IMAGE: ${REGISTRY}/motovaultpro/frontend:${IMAGE_TAG} - - # Deployment configuration - GIT_CLONE_PATH: ${CI_BUILDS_DIR}/motovaultpro - DEPLOY_PATH: ${CI_BUILDS_DIR}/motovaultpro - COMPOSE_FILE: docker-compose.yml - COMPOSE_BLUE_GREEN: docker-compose.blue-green.yml - - # Health check configuration - HEALTH_CHECK_TIMEOUT: "60" - -# Default after_script to fix permissions -default: - after_script: - - echo "Fixing file permissions..." - - sudo chown -R gitlab-runner:gitlab-runner "$DEPLOY_PATH" 2>/dev/null || true - - sudo chown -R 1001:1001 "$DEPLOY_PATH/data/backups" "$DEPLOY_PATH/data/documents" 2>/dev/null || true - -# ============================================ -# Stage 1: VALIDATE -# Check prerequisites before starting pipeline -# ============================================ -validate: - stage: validate - tags: - - production - - shell - only: - - main - script: - - echo "==========================================" - - echo "Validating deployment prerequisites..." - - echo "==========================================" - - echo "Checking Docker..." - - docker info > /dev/null 2>&1 || (echo "ERROR - Docker not accessible" && exit 1) - - echo "OK - Docker is accessible" - - echo "Checking Docker Compose..." - - docker compose version > /dev/null 2>&1 || (echo "ERROR - Docker Compose not available" && exit 1) - - echo "OK - Docker Compose is available" - - echo "Checking deployment path..." - - test -d "$DEPLOY_PATH" || (echo "ERROR - DEPLOY_PATH not found" && exit 1) - - echo "OK - Deployment path exists" - - echo "Checking registry access..." - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY" || true - - echo "OK - Registry authentication configured" - - echo "Determining target stack..." - - | - STATE_FILE="$DEPLOY_PATH/config/deployment/state.json" - if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then - ACTIVE_STACK=$(jq -r '.active_stack // "blue"' "$STATE_FILE") - if [ "$ACTIVE_STACK" = "blue" ]; then - echo "TARGET_STACK=green" >> deploy.env - else - echo "TARGET_STACK=blue" >> deploy.env - fi - else - echo "TARGET_STACK=green" >> deploy.env - fi - cat deploy.env - - echo "==========================================" - - echo "Validation complete" - - echo "==========================================" - artifacts: - reports: - dotenv: deploy.env - -# ============================================ -# Stage 2: BUILD -# Build and push images to GitLab Container Registry -# Runs on dedicated build server (shell executor) -# ============================================ -build: - stage: build - tags: - - build - only: - - main - script: - - echo "Authenticating with GitLab Container Registry..." - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY" - - echo "==========================================" - - echo "Building Docker images..." - - echo "Commit - ${CI_COMMIT_SHORT_SHA}" - - echo "Backend - ${BACKEND_IMAGE}" - - echo "Frontend - ${FRONTEND_IMAGE}" - - echo "==========================================" - - # Build backend - - echo "Building backend..." - - | - docker build \ - --build-arg BUILDKIT_INLINE_CACHE=1 \ - --cache-from ${REGISTRY}/motovaultpro/backend:latest \ - -t ${BACKEND_IMAGE} \ - -t ${REGISTRY}/motovaultpro/backend:latest \ - -f backend/Dockerfile \ - . - - # Build frontend - - echo "Building frontend..." - - | - docker build \ - --build-arg BUILDKIT_INLINE_CACHE=1 \ - --build-arg VITE_AUTH0_DOMAIN=${VITE_AUTH0_DOMAIN:-motovaultpro.us.auth0.com} \ - --build-arg VITE_AUTH0_CLIENT_ID=${VITE_AUTH0_CLIENT_ID:-yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3} \ - --build-arg VITE_AUTH0_AUDIENCE=${VITE_AUTH0_AUDIENCE:-https://api.motovaultpro.com} \ - --build-arg VITE_API_BASE_URL=/api \ - --cache-from ${REGISTRY}/motovaultpro/frontend:latest \ - -t ${FRONTEND_IMAGE} \ - -t ${REGISTRY}/motovaultpro/frontend:latest \ - -f frontend/Dockerfile \ - frontend - - # Push images - - echo "Pushing images to registry..." - - docker push ${BACKEND_IMAGE} - - docker push ${FRONTEND_IMAGE} - - docker push ${REGISTRY}/motovaultpro/backend:latest - - docker push ${REGISTRY}/motovaultpro/frontend:latest - - - echo "==========================================" - - echo "Build complete" - - echo "==========================================" - -# ============================================ -# Stage 3: DEPLOY-PREPARE -# Pull images, start inactive stack, run health checks -# ============================================ -deploy-prepare: - stage: deploy-prepare - tags: - - production - - shell - only: - - main - needs: - - job: validate - artifacts: true - - job: build - environment: - name: production - url: https://motovaultpro.com - script: - - echo "==========================================" - - echo "Preparing deployment to ${TARGET_STACK} stack..." - - echo "==========================================" - - cd "$DEPLOY_PATH" - - # Authenticate with registry - - echo "Authenticating with registry..." - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY" - - # Inject secrets - - echo "Step 1/5 - Injecting secrets..." - - chmod +x scripts/inject-secrets.sh - - ./scripts/inject-secrets.sh - - # Initialize data directories - - echo "Step 2/5 - Initializing data directories..." - - sudo mkdir -p data/backups data/documents - - sudo chown -R 1001:1001 data/backups data/documents - - sudo chmod 755 data/backups data/documents - - # Pull new images - - echo "Step 3/5 - Pulling images..." - - docker pull ${BACKEND_IMAGE} - - docker pull ${FRONTEND_IMAGE} - - # Start inactive stack - - echo "Step 4/5 - Starting ${TARGET_STACK} stack..." - - | - export BACKEND_IMAGE=${BACKEND_IMAGE} - export FRONTEND_IMAGE=${FRONTEND_IMAGE} - docker compose -f $COMPOSE_FILE -f $COMPOSE_BLUE_GREEN up -d \ - mvp-frontend-${TARGET_STACK} mvp-backend-${TARGET_STACK} - - # Wait for stack to be ready - - echo "Step 5/5 - Waiting for stack health..." - - sleep 10 - - # Run health check - - echo "Running health check on ${TARGET_STACK} stack..." - - chmod +x scripts/ci/health-check.sh - - ./scripts/ci/health-check.sh ${TARGET_STACK} ${HEALTH_CHECK_TIMEOUT} - - # Update state with deployment info - - | - STATE_FILE="config/deployment/state.json" - if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then - TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - jq --arg stack "$TARGET_STACK" \ - --arg commit "$CI_COMMIT_SHORT_SHA" \ - --arg ts "$TIMESTAMP" \ - '.[$stack].version = $commit | .[$stack].commit = $commit | .[$stack].deployed_at = $ts | .[$stack].healthy = true' \ - "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" - fi - - - echo "==========================================" - - echo "Deploy preparation complete" - - echo "==========================================" - -# ============================================ -# Stage 4: DEPLOY-SWITCH -# Switch traffic to new stack -# ============================================ -deploy-switch: - stage: deploy-switch - tags: - - production - - shell - only: - - main - needs: - - job: validate - artifacts: true - - job: deploy-prepare - script: - - echo "==========================================" - - echo "Switching traffic to ${TARGET_STACK} stack..." - - echo "==========================================" - - cd "$DEPLOY_PATH" - - # Switch traffic - - chmod +x scripts/ci/switch-traffic.sh - - ./scripts/ci/switch-traffic.sh ${TARGET_STACK} instant - - # Update state - - | - STATE_FILE="config/deployment/state.json" - if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then - TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - jq --arg commit "$CI_COMMIT_SHORT_SHA" \ - --arg ts "$TIMESTAMP" \ - '.last_deployment = $ts | .last_deployment_commit = $commit | .last_deployment_status = "success" | .rollback_available = true' \ - "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" - fi - - - echo "==========================================" - - echo "Traffic switch complete" - - echo "==========================================" - -# ============================================ -# Stage 5: VERIFY -# Production health verification after switch -# ============================================ -verify: - stage: verify - tags: - - production - - shell - only: - - main - needs: - - job: validate - artifacts: true - - job: deploy-switch - script: - - echo "==========================================" - - echo "Verifying production deployment..." - - echo "==========================================" - - cd "$DEPLOY_PATH" - - # Wait for Traefik to propagate routing - - echo "Waiting for traffic routing to stabilize..." - - sleep 5 - - # Verify via external endpoint - - echo "Checking external endpoint..." - - | - for i in 1 2 3 4 5 6; do - if curl -sf https://motovaultpro.com/api/health > /dev/null 2>&1; then - echo "OK - External health check passed" - break - fi - if [ $i -eq 6 ]; then - echo "ERROR - External health check failed after 6 attempts" - exit 1 - fi - echo "Attempt $i/6 - Waiting 10s..." - sleep 10 - done - - # Verify container status - - echo "Checking container status..." - - | - for service in mvp-frontend-${TARGET_STACK} mvp-backend-${TARGET_STACK}; do - status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found") - health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown") - if [ "$status" != "running" ] || [ "$health" != "healthy" ]; then - echo "ERROR - $service is not healthy (status: $status, health: $health)" - docker logs $service --tail 50 2>/dev/null || true - exit 1 - fi - echo "OK - $service is running and healthy" - done - - - echo "==========================================" - - echo "Deployment verified successfully!" - - echo "Version ${CI_COMMIT_SHORT_SHA} is now live" - - echo "==========================================" - -# ============================================ -# Stage 6: ROLLBACK (on failure) -# Automatic rollback if verify stage fails -# ============================================ -rollback: - stage: rollback - tags: - - production - - shell - only: - - main - when: on_failure - needs: - - job: validate - artifacts: true - - job: deploy-switch - - job: verify - script: - - echo "==========================================" - - echo "INITIATING AUTO-ROLLBACK" - - echo "==========================================" - - cd "$DEPLOY_PATH" - - # Run rollback script - - chmod +x scripts/ci/auto-rollback.sh - - ./scripts/ci/auto-rollback.sh "Verify stage failed - automatic rollback" - - # Update state - - | - STATE_FILE="config/deployment/state.json" - if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then - jq '.last_deployment_status = "rolled_back"' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" - fi - - - echo "==========================================" - - echo "Rollback complete" - - echo "==========================================" - -# ============================================ -# Stage 7: NOTIFY -# Send deployment notifications -# ============================================ -notify-success: - stage: notify - tags: - - production - - shell - only: - - main - needs: - - job: verify - script: - - echo "Sending success notification..." - - cd "$DEPLOY_PATH" - - chmod +x scripts/ci/notify.sh - - ./scripts/ci/notify.sh success "Version ${CI_COMMIT_SHORT_SHA} deployed successfully" ${CI_COMMIT_SHORT_SHA} - -notify-failure: - stage: notify - tags: - - production - - shell - only: - - main - when: on_failure - needs: - - job: build - optional: true - - job: deploy-prepare - optional: true - - job: deploy-switch - optional: true - - job: verify - optional: true - script: - - echo "Sending failure notification..." - - cd "$DEPLOY_PATH" - - chmod +x scripts/ci/notify.sh - - ./scripts/ci/notify.sh failure "Deployment of ${CI_COMMIT_SHORT_SHA} failed" ${CI_COMMIT_SHORT_SHA} - -# ============================================ -# Manual Jobs -# ============================================ - -# Manual maintenance migration job -maintenance-migration: - stage: deploy-prepare - tags: - - production - - shell - only: - - main - when: manual - script: - - echo "==========================================" - - echo "MAINTENANCE MODE MIGRATION" - - echo "==========================================" - - cd "$DEPLOY_PATH" - - chmod +x scripts/ci/maintenance-migrate.sh - - ./scripts/ci/maintenance-migrate.sh backup - -# Mirror base images (scheduled or manual) -mirror-images: - stage: build - tags: - - build - only: - - schedules - - web - when: manual - script: - - echo "Mirroring base images to GitLab Container Registry..." - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY" - - chmod +x scripts/ci/mirror-base-images.sh - - REGISTRY=${REGISTRY}/mirrors ./scripts/ci/mirror-base-images.sh diff --git a/backend/Dockerfile b/backend/Dockerfile index 7de3b33..c0f35ee 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,8 @@ # Production Dockerfile for MotoVaultPro Backend -# Uses mirrored base images from GitLab Container Registry +# Uses mirrored base images from Gitea Package Registry -# Build argument for registry (defaults to GitLab mirrors, falls back to Docker Hub) -ARG REGISTRY_MIRRORS=registry.motovaultpro.com/mirrors +# Build argument for registry (defaults to Gitea mirrors, falls back to Docker Hub) +ARG REGISTRY_MIRRORS=git.motovaultpro.com/egullickson/mirrors # Stage 1: Build stage FROM ${REGISTRY_MIRRORS}/node:20-alpine AS builder diff --git a/docker-compose.blue-green.yml b/docker-compose.blue-green.yml index 977aee3..b961168 100644 --- a/docker-compose.blue-green.yml +++ b/docker-compose.blue-green.yml @@ -16,7 +16,7 @@ services: # BLUE Stack - Frontend # ======================================== mvp-frontend-blue: - image: ${FRONTEND_IMAGE:-registry.motovaultpro.com/motovaultpro/frontend:latest} + image: ${FRONTEND_IMAGE:-git.motovaultpro.com/egullickson/frontend:latest} container_name: mvp-frontend-blue restart: unless-stopped environment: @@ -52,7 +52,7 @@ services: # BLUE Stack - Backend # ======================================== mvp-backend-blue: - image: ${BACKEND_IMAGE:-registry.motovaultpro.com/motovaultpro/backend:latest} + image: ${BACKEND_IMAGE:-git.motovaultpro.com/egullickson/backend:latest} container_name: mvp-backend-blue restart: unless-stopped environment: @@ -101,7 +101,7 @@ services: # GREEN Stack - Frontend # ======================================== mvp-frontend-green: - image: ${FRONTEND_IMAGE:-registry.motovaultpro.com/motovaultpro/frontend:latest} + image: ${FRONTEND_IMAGE:-git.motovaultpro.com/egullickson/frontend:latest} container_name: mvp-frontend-green restart: unless-stopped environment: @@ -137,7 +137,7 @@ services: # GREEN Stack - Backend # ======================================== mvp-backend-green: - image: ${BACKEND_IMAGE:-registry.motovaultpro.com/motovaultpro/backend:latest} + image: ${BACKEND_IMAGE:-git.motovaultpro.com/egullickson/backend:latest} container_name: mvp-backend-green restart: unless-stopped environment: diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..cc4b780 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,80 @@ +# Staging Environment Docker Compose +# Runs full application stack on staging server (staging.motovaultpro.com) +# Usage: docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d +# +# Differences from production: +# - Single stack (no blue-green) +# - Staging domain (staging.motovaultpro.com) +# - Separate database (isolated from production) +# - Uses same images as production for accurate testing + +services: + # ======================================== + # Traefik - Reverse Proxy (Staging) + # ======================================== + mvp-traefik: + image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/traefik:v3.6 + container_name: mvp-traefik-staging + labels: + - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.staging.motovaultpro.com`)" + + # ======================================== + # Frontend (Staging) + # ======================================== + mvp-frontend: + image: ${FRONTEND_IMAGE:-git.motovaultpro.com/egullickson/frontend:latest} + container_name: mvp-frontend-staging + labels: + - "traefik.enable=true" + - "traefik.docker.network=motovaultpro_frontend" + - "traefik.http.routers.mvp-frontend.rule=Host(`staging.motovaultpro.com`) && !PathPrefix(`/api`)" + - "traefik.http.routers.mvp-frontend.entrypoints=websecure" + - "traefik.http.routers.mvp-frontend.tls=true" + - "traefik.http.routers.mvp-frontend.tls.certresolver=letsencrypt" + - "traefik.http.routers.mvp-frontend.priority=10" + - "traefik.http.services.mvp-frontend.loadbalancer.server.port=3000" + + # ======================================== + # Backend (Staging) + # ======================================== + mvp-backend: + image: ${BACKEND_IMAGE:-git.motovaultpro.com/egullickson/backend:latest} + container_name: mvp-backend-staging + labels: + - "traefik.enable=true" + - "traefik.docker.network=motovaultpro_backend" + - "traefik.http.routers.mvp-backend.rule=Host(`staging.motovaultpro.com`) && PathPrefix(`/api`)" + - "traefik.http.routers.mvp-backend.entrypoints=websecure" + - "traefik.http.routers.mvp-backend.tls=true" + - "traefik.http.routers.mvp-backend.tls.certresolver=letsencrypt" + - "traefik.http.routers.mvp-backend.priority=20" + - "traefik.http.routers.mvp-backend-health.rule=Host(`staging.motovaultpro.com`) && Path(`/api/health`)" + - "traefik.http.routers.mvp-backend-health.entrypoints=websecure" + - "traefik.http.routers.mvp-backend-health.tls=true" + - "traefik.http.routers.mvp-backend-health.tls.certresolver=letsencrypt" + - "traefik.http.routers.mvp-backend-health.priority=30" + - "traefik.http.services.mvp-backend.loadbalancer.server.port=3001" + + # ======================================== + # PostgreSQL (Staging - Separate Database) + # ======================================== + mvp-postgres: + container_name: mvp-postgres-staging + volumes: + - mvp_postgres_staging_data:/var/lib/postgresql/data + - ./secrets/staging/postgres-password.txt:/run/secrets/postgres-password:ro + + # ======================================== + # Redis (Staging) + # ======================================== + mvp-redis: + container_name: mvp-redis-staging + volumes: + - mvp_redis_staging_data:/data + +# Staging-specific volumes (separate from production) +volumes: + mvp_postgres_staging_data: + name: mvp_postgres_staging_data + mvp_redis_staging_data: + name: mvp_redis_staging_data diff --git a/docker-compose.yml b/docker-compose.yml index 79c131f..ff72f9e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,11 @@ # Base registry for mirrored images (override with environment variable) x-registry: ®istry - REGISTRY_MIRRORS: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors} + REGISTRY_MIRRORS: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors} services: # Traefik - Service Discovery and Load Balancing mvp-traefik: - image: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors}/traefik:v3.6 + image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/traefik:v3.6 container_name: mvp-traefik restart: unless-stopped command: @@ -158,7 +158,7 @@ services: # Database Services - Application PostgreSQL mvp-postgres: - image: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors}/postgres:18-alpine + image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/postgres:18-alpine container_name: mvp-postgres restart: unless-stopped environment: @@ -183,7 +183,7 @@ services: # Database Services - Application Redis mvp-redis: - image: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors}/redis:8.4-alpine + image: ${REGISTRY_MIRRORS:-git.motovaultpro.com/egullickson/mirrors}/redis:8.4-alpine container_name: mvp-redis restart: unless-stopped command: redis-server --appendonly yes diff --git a/docs/BUILD-SERVER-SETUP.md b/docs/BUILD-SERVER-SETUP.md index 300cc04..84362f0 100644 --- a/docs/BUILD-SERVER-SETUP.md +++ b/docs/BUILD-SERVER-SETUP.md @@ -1,22 +1,28 @@ -# Build Server Setup Guide +# Build/Staging Server Setup Guide -Complete guide for setting up a dedicated build VPS for MotoVaultPro CI/CD pipeline. +Complete guide for setting up the build and staging server for MotoVaultPro CI/CD with Gitea Actions. ## Overview -The build server isolates resource-intensive Docker builds from the production server, ensuring deployments don't impact application performance. +The build server serves dual purposes: +1. **Build Server**: Builds Docker images and pushes to Gitea Package Registry +2. **Staging Server**: Runs full application stack at staging.motovaultpro.com ``` +-------------------+ +--------------------+ -| GitLab Server | | Production Server | -| (CI/CD + Registry)| | (Shell Runner) | -+--------+----------+ +----------+---------+ - | | - v v -+--------+----------+ +----------+---------+ -| Build VPS | | Blue-Green Stacks | -| (Docker Runner) |---->| + Shared Data | -+-------------------+ +--------------------+ +| Gitea Server | | Production Server | +| git.motovaultpro | | (mvp-prod runner) | +| + Package Registry| +----------+---------+ ++--------+----------+ | + | v + v motovaultpro.com ++--------+----------+ +| Build/Staging VPS | +| (mvp-build runner)| ++-------------------+ + | + v +staging.motovaultpro.com ``` ## Server Requirements @@ -25,16 +31,16 @@ The build server isolates resource-intensive Docker builds from the production s | Resource | Requirement | |----------|-------------| -| CPU | 2 cores | -| RAM | 4GB | -| Storage | 50GB SSD | +| CPU | 4 cores | +| RAM | 8GB | +| Storage | 100GB SSD | | Network | 100Mbps+ | | OS | Ubuntu 22.04 LTS / Debian 12 | ### Network Requirements -- Outbound HTTPS to GitLab instance -- Outbound HTTPS to Docker registries (for fallback) +- Port 80/443 open (for staging.motovaultpro.com) +- Outbound HTTPS to git.motovaultpro.com - SSH access for administration --- @@ -45,7 +51,7 @@ The build server isolates resource-intensive Docker builds from the production s ```bash sudo apt update && sudo apt upgrade -y -sudo apt install -y curl git ca-certificates gnupg +sudo apt install -y curl git ca-certificates gnupg jq ``` ### 2. Install Docker Engine @@ -56,7 +62,7 @@ sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg -# Add the repository to Apt sources +# Add the repository echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ @@ -71,95 +77,162 @@ docker --version docker compose version ``` -### 3. Install GitLab Runner +### 3. Install act_runner ```bash -# Add GitLab Runner repository -curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash - -# Install GitLab Runner -sudo apt install gitlab-runner +# Download act_runner binary +curl -L https://gitea.com/gitea/act_runner/releases/download/v0.2.12/act_runner-0.2.12-linux-amd64 -o /tmp/act_runner +sudo mv /tmp/act_runner /usr/local/bin/act_runner +sudo chmod +x /usr/local/bin/act_runner # Verify installation -gitlab-runner --version +act_runner --version ``` -### 4. Register Runner with Shell Executor +### 4. Create act_runner User ```bash -sudo gitlab-runner register \ - --non-interactive \ - --url "https://git.motovaultpro.com" \ - --registration-token "YOUR_REGISTRATION_TOKEN" \ - --executor "shell" \ - --description "Build Server - Shell Executor" \ - --tag-list "build" \ - --run-untagged="false" \ - --locked="true" +# Create user for running act_runner +sudo useradd -r -m -s /bin/bash act_runner +sudo usermod -aG docker act_runner + +# Create config directory +sudo mkdir -p /etc/act_runner +sudo chown act_runner:act_runner /etc/act_runner ``` -**Notes:** -- Replace `YOUR_REGISTRATION_TOKEN` with the token from GitLab Admin > CI/CD > Runners -- Shell executor runs jobs directly on the host with access to Docker -- Tag `build` is used in `.gitlab-ci.yml` to route build jobs to this server +### 5. Register Runner with Gitea -### 5. Add gitlab-runner to Docker Group - -The gitlab-runner user needs access to Docker: +Get a registration token from: `git.motovaultpro.com/egullickson/motovaultpro/settings/actions/runners` ```bash -sudo usermod -aG docker gitlab-runner +# Generate config +sudo -u act_runner act_runner generate-config > /etc/act_runner/config.yaml -# Verify access -sudo -u gitlab-runner docker info -sudo -u gitlab-runner docker compose version +# Register runner with staging/build label +sudo -u act_runner act_runner register --no-interactive \ + --instance https://git.motovaultpro.com \ + --token \ + --name "Build/Staging Server" \ + --labels "mvp-build:host" ``` -### 6. Configure Docker Registry Authentication - -Create credentials file for GitLab Container Registry: +### 6. Create Systemd Service ```bash -# Login to GitLab Container Registry (creates ~/.docker/config.json) -docker login registry.motovaultpro.com -u -p +cat << 'EOF' | sudo tee /etc/systemd/system/act_runner.service +[Unit] +Description=Gitea Actions Runner +After=docker.service network.target + +[Service] +ExecStart=/usr/local/bin/act_runner daemon --config /etc/act_runner/config.yaml +WorkingDirectory=/home/act_runner +User=act_runner +Group=act_runner +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + +# Enable and start +sudo systemctl daemon-reload +sudo systemctl enable act_runner --now +sudo systemctl status act_runner ``` -**Creating Deploy Token:** -1. Go to GitLab Project > Settings > Repository > Deploy Tokens -2. Create token with `read_registry` and `write_registry` scopes -3. Use the token username/password for Docker login +--- + +## Staging Environment Setup + +### 1. Clone Repository + +```bash +sudo mkdir -p /opt/motovaultpro +sudo chown act_runner:act_runner /opt/motovaultpro +sudo -u act_runner git clone https://git.motovaultpro.com/egullickson/motovaultpro.git /opt/motovaultpro +``` + +### 2. Create Staging Secrets Directory + +```bash +sudo mkdir -p /opt/motovaultpro/secrets/staging +sudo chown -R act_runner:act_runner /opt/motovaultpro/secrets +sudo chmod 700 /opt/motovaultpro/secrets/staging +``` + +### 3. Configure DNS + +Add DNS A record: +``` +staging.motovaultpro.com -> +``` + +### 4. Configure Cloudflare (if using) + +Ensure `staging.motovaultpro.com` is proxied through Cloudflare or has a valid SSL certificate configured. + +### 5. Initialize Data Directories + +```bash +cd /opt/motovaultpro +sudo mkdir -p data/backups data/documents +sudo chown -R 1001:1001 data/backups data/documents +``` + +--- + +## Docker Registry Authentication + +### Login to Gitea Package Registry + +```bash +# Login as act_runner user +sudo -u act_runner docker login git.motovaultpro.com -u egullickson +# Enter your Gitea access token when prompted +``` + +### Create Access Token + +1. Go to `git.motovaultpro.com/user/settings/applications` +2. Create new token with scopes: + - `read:packages` + - `write:packages` +3. Save token securely --- ## Verification -### Test Runner Registration +### Check Runner Status ```bash -sudo gitlab-runner verify +sudo systemctl status act_runner ``` -Expected output: -``` -Verifying runner... is alive runner=XXXXXX -``` +### Check Runner Registration + +Go to `git.motovaultpro.com/egullickson/motovaultpro/settings/actions/runners` and verify the runner appears as "Online". ### Test Docker Access ```bash -sudo gitlab-runner exec docker --docker-privileged test-job +sudo -u act_runner docker info +sudo -u act_runner docker compose version ``` ### Test Registry Push ```bash # Build and push a test image -docker build -t registry.motovaultpro.com/motovaultpro/test:latest -f- . < /dev/null <<'EOF' @@ -185,102 +256,64 @@ sudo chmod +x /usr/local/bin/docker-cleanup.sh echo "0 3 * * * /usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1" | sudo crontab - ``` -### Log Rotation - -Configure log rotation for GitLab Runner: - -```bash -sudo tee /etc/logrotate.d/gitlab-runner > /dev/null < build -> deploy-prepare -> deploy-switch -> verify -> [rollback] -> notify -``` +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| `staging.yaml` | Push to main | Build, deploy to staging, verify | +| `production.yaml` | Manual (workflow_dispatch) | Deploy to production | -| Stage | Runner | Purpose | -|-------|--------|---------| -| `validate` | prod | Check prerequisites, determine target stack | -| `build` | build | Build and push images to GitLab registry | -| `deploy-prepare` | prod | Pull images, start inactive stack, health check | -| `deploy-switch` | prod | Switch Traefik traffic weights | -| `verify` | prod | Production health verification | -| `rollback` | prod | Auto-triggered on verify failure | -| `notify` | prod | Email success/failure notifications | - -### Pipeline Flow +### Staging Workflow (Automatic) ``` [Push to main] | v -[validate] - Checks Docker, paths, registry +[build] -------- Build images, push to registry | v -[build] - Builds backend + frontend images - | Pushes to registry.motovaultpro.com - v -[deploy-prepare] - Pulls new images - | Starts inactive stack (blue or green) - | Runs health checks - v -[deploy-switch] - Updates Traefik weights - | Switches traffic instantly - v -[verify] - External health check - | Container status verification +[deploy-staging] - Deploy full stack to staging | - +--[SUCCESS]--> [notify-success] - Sends success email + v +[verify-staging] - Health checks | - +--[FAILURE]--> [rollback] - Switches back to previous stack - | - v - [notify-failure] - Sends failure email + +--[FAIL]--> [notify-staging-failure] + | + v +[notify-staging-ready] - Email with production deploy link +``` + +### Production Workflow (Manual) + +``` +[Manual Trigger] - User clicks "Run workflow" + | + v +[validate] - Check prerequisites, determine stack + | + v +[deploy-prod] - Blue-green deployment + | + v +[verify-prod] - External health checks + | + +--[FAIL]--> [rollback] --> [notify-failure] + | + v +[notify-success] ``` --- -## Blue-Green Deployment +## Deployment Process -### Stack Configuration +### 1. Push to Main Branch -Both stacks share the same database layer: +When you push to `main`: +1. Staging workflow triggers automatically +2. Images are built and pushed to Gitea Package Registry +3. Full stack deploys to staging.motovaultpro.com +4. Health checks verify staging works +5. Email notification sent with production deploy link -| Component | Blue Stack | Green Stack | Shared | -|-----------|------------|-------------|--------| -| Frontend | `mvp-frontend-blue` | `mvp-frontend-green` | - | -| Backend | `mvp-backend-blue` | `mvp-backend-green` | - | -| PostgreSQL | - | - | `mvp-postgres` | -| Redis | - | - | `mvp-redis` | -| Traefik | - | - | `mvp-traefik` | +### 2. Review Staging -### Traffic Routing +After receiving the "Staging Ready" email: +1. Visit https://staging.motovaultpro.com +2. Test functionality +3. Review logs if needed: `docker logs mvp-backend-staging` -Traefik uses weighted services for traffic distribution: +### 3. Deploy to Production -```yaml -# config/traefik/dynamic/blue-green.yml -services: - mvp-frontend-weighted: - weighted: - services: - - name: mvp-frontend-blue-svc - weight: 100 # Active - - name: mvp-frontend-green-svc - weight: 0 # Standby -``` +When ready to deploy: +1. Go to `git.motovaultpro.com/egullickson/motovaultpro/actions` +2. Select "Deploy to Production" workflow +3. Click "Run workflow" +4. Optionally specify image tag (defaults to `latest`) +5. Click "Run workflow" to confirm -### Deployment State +### 4. Monitor Production Deploy -State is tracked in `config/deployment/state.json`: - -```json -{ - "active_stack": "blue", - "inactive_stack": "green", - "last_deployment": "2024-01-15T10:30:00Z", - "last_deployment_commit": "abc123", - "rollback_available": true -} -``` +The production workflow will: +1. Determine target stack (blue or green) +2. Pull and start the new stack +3. Run health checks +4. Switch traffic +5. Verify external health +6. Auto-rollback if verification fails +7. Send email notification --- -## CI/CD Variables Configuration +## Secrets and Variables -Navigate to **Settings > CI/CD > Variables** in your GitLab project. +### Secrets Configuration -### Required Variables +Navigate to: `git.motovaultpro.com/egullickson/motovaultpro/settings/actions/secrets` -| Variable | Type | Protected | Purpose | -|----------|------|-----------|---------| -| `DEPLOY_NOTIFY_EMAIL` | Variable | Yes | Notification recipient | -| `VITE_AUTH0_DOMAIN` | Variable | No | Auth0 domain | -| `VITE_AUTH0_CLIENT_ID` | Variable | No | Auth0 client ID | -| `VITE_AUTH0_AUDIENCE` | Variable | No | Auth0 audience | +| Secret | Description | +|--------|-------------| +| `REGISTRY_USER` | Gitea username (egullickson) | +| `REGISTRY_PASSWORD` | Gitea access token | +| `POSTGRES_PASSWORD` | Production PostgreSQL password | +| `STAGING_POSTGRES_PASSWORD` | Staging PostgreSQL password | +| `AUTH0_CLIENT_SECRET` | Auth0 secret | +| `AUTH0_MANAGEMENT_CLIENT_ID` | Auth0 Management API ID | +| `AUTH0_MANAGEMENT_CLIENT_SECRET` | Auth0 Management API secret | +| `GOOGLE_MAPS_API_KEY` | Google Maps key | +| `GOOGLE_MAPS_MAP_ID` | Google Maps Map ID | +| `CF_DNS_API_TOKEN` | Cloudflare DNS token | +| `RESEND_API_KEY` | Resend email API key | -### Secret Files +### Variables Configuration -These use GitLab's **File** type and are injected via `scripts/inject-secrets.sh`: +Navigate to: `git.motovaultpro.com/egullickson/motovaultpro/settings/actions/variables` -| Variable | Type | Protected | Masked | -|----------|------|-----------|--------| -| `POSTGRES_PASSWORD` | File | Yes | Yes | -| `AUTH0_CLIENT_SECRET` | File | Yes | Yes | -| `GOOGLE_MAPS_API_KEY` | File | Yes | Yes | -| `GOOGLE_MAPS_MAP_ID` | File | Yes | No | -| `CF_DNS_API_TOKEN` | File | Yes | Yes | -| `RESEND_API_KEY` | File | Yes | Yes | - -### Registry Authentication - -GitLab provides these automatically: -- `CI_REGISTRY_USER` - Registry username -- `CI_REGISTRY_PASSWORD` - Registry token -- `CI_REGISTRY` - Registry URL +| Variable | Value | +|----------|-------| +| `DEPLOY_NOTIFY_EMAIL` | Notification recipient | +| `VITE_AUTH0_DOMAIN` | motovaultpro.us.auth0.com | +| `VITE_AUTH0_CLIENT_ID` | Auth0 client ID | +| `VITE_AUTH0_AUDIENCE` | https://api.motovaultpro.com | --- ## Container Registry -All images are hosted on the GitLab Container Registry to avoid Docker Hub rate limits. +All images are hosted on Gitea Package Registry. ### Registry URL ``` -registry.motovaultpro.com +git.motovaultpro.com ``` ### Image Paths | Image | Path | |-------|------| -| Backend | `registry.motovaultpro.com/motovaultpro/backend:$TAG` | -| Frontend | `registry.motovaultpro.com/motovaultpro/frontend:$TAG` | -| Mirrors | `registry.motovaultpro.com/mirrors/` | +| Backend | `git.motovaultpro.com/egullickson/backend:$TAG` | +| Frontend | `git.motovaultpro.com/egullickson/frontend:$TAG` | +| Mirrors | `git.motovaultpro.com/egullickson/mirrors/` | ### Base Image Mirrors -Mirror upstream images to avoid rate limits: +Run the mirror workflow to avoid Docker Hub rate limits: -```bash -# Run manually or via scheduled pipeline -./scripts/ci/mirror-base-images.sh -``` +1. Go to Actions tab +2. Select "Mirror Base Images" +3. Click "Run workflow" Mirrored images: - `node:20-alpine` @@ -255,41 +227,6 @@ Mirrored images: - `postgres:18-alpine` - `redis:8.4-alpine` - `traefik:v3.6` -- `docker:24.0` -- `docker:24.0-dind` - ---- - -## Deployment Process - -### Automatic Deployment - -Deployments trigger automatically on push to `main`: - -1. **Validate**: Check prerequisites, determine target stack -2. **Build**: Build images on dedicated build server -3. **Prepare**: Start inactive stack, run health checks -4. **Switch**: Update Traefik weights (instant) -5. **Verify**: External health check -6. **Notify**: Send email notification - -### Manual Deployment - -1. Go to **CI/CD > Pipelines** -2. Click **Run pipeline** -3. Select `main` branch -4. Click **Run pipeline** - -### Deployment Timeline - -| Phase | Duration | -|-------|----------| -| Validate | ~5s | -| Build | ~2 min | -| Deploy-prepare | ~30s | -| Deploy-switch | ~3s | -| Verify | ~30s | -| **Total** | ~3 min | --- @@ -297,15 +234,9 @@ Deployments trigger automatically on push to `main`: ### Automatic Rollback -Triggers automatically when: -- Health check fails in `deploy-prepare` -- `verify` stage fails after switch -- Container becomes unhealthy within verification period - -The pipeline runs `scripts/ci/auto-rollback.sh` which: -1. Verifies previous stack is healthy -2. Switches traffic back -3. Sends notification +Production workflow auto-rolls back when: +- Health check fails after traffic switch +- Container becomes unhealthy during verification ### Manual Rollback @@ -326,6 +257,8 @@ cat config/deployment/state.json | jq . If both stacks are unhealthy: ```bash +cd /opt/motovaultpro + # Stop everything docker compose -f docker-compose.yml -f docker-compose.blue-green.yml down @@ -336,8 +269,8 @@ docker compose up -d mvp-postgres mvp-redis mvp-traefik sleep 15 # Start one stack -export BACKEND_IMAGE=registry.motovaultpro.com/motovaultpro/backend:latest -export FRONTEND_IMAGE=registry.motovaultpro.com/motovaultpro/frontend:latest +export BACKEND_IMAGE=git.motovaultpro.com/egullickson/backend:latest +export FRONTEND_IMAGE=git.motovaultpro.com/egullickson/frontend:latest docker compose -f docker-compose.yml -f docker-compose.blue-green.yml up -d \ mvp-frontend-blue mvp-backend-blue @@ -351,11 +284,13 @@ docker compose -f docker-compose.yml -f docker-compose.blue-green.yml up -d \ For breaking database changes requiring downtime: -### Via Pipeline (Recommended) +### Via Gitea Actions -1. Go to **CI/CD > Pipelines** -2. Find the `maintenance-migration` job -3. Click **Play** to trigger manually +1. Go to Actions tab +2. Select "Maintenance Migration" +3. Click "Run workflow" +4. Choose whether to create backup +5. Click "Run workflow" ### Via Script @@ -369,98 +304,68 @@ cd /opt/motovaultpro ./scripts/ci/maintenance-migrate.sh ``` -### What Happens - -1. Sends maintenance notification -2. Enables maintenance mode (stops traffic) -3. Creates database backup (if requested) -4. Runs migrations -5. Restarts backends -6. Restores traffic -7. Sends completion notification - ---- - -## Notifications - -Email notifications via Resend API for: - -| Event | Subject | -|-------|---------| -| `success` | Deployment Successful | -| `failure` | Deployment Failed | -| `rollback` | Auto-Rollback Executed | -| `rollback_failed` | CRITICAL: Rollback Failed | -| `maintenance_start` | Maintenance Mode Started | -| `maintenance_end` | Maintenance Complete | - -Configure recipient in GitLab CI/CD variables: -``` -DEPLOY_NOTIFY_EMAIL = admin@example.com -``` - --- ## Troubleshooting -### Pipeline Fails at Build Stage +### Staging Workflow Failures -**Check build server connectivity:** +**Build fails:** ```bash # On build server -sudo gitlab-runner verify -docker login registry.motovaultpro.com -``` - -**Check disk space:** -```bash -df -h +docker system df docker system prune -af ``` -### Pipeline Fails at Deploy-Prepare - -**Container won't start:** +**Deploy fails:** ```bash -docker logs mvp-backend-blue --tail 100 -docker logs mvp-frontend-blue --tail 100 +docker logs mvp-backend-staging +docker logs mvp-frontend-staging ``` +### Production Workflow Failures + **Health check timeout:** ```bash -# Increase timeout in .gitlab-ci.yml -HEALTH_CHECK_TIMEOUT: "90" +# Check containers +docker ps +docker logs mvp-backend-blue # or green ``` -### Traffic Not Switching - -**Check Traefik config:** +**Traffic not switching:** ```bash +# Check Traefik config cat config/traefik/dynamic/blue-green.yml docker exec mvp-traefik traefik healthcheck ``` -**Check routing:** +### Runner Issues + +**Runner offline:** ```bash -curl -I https://motovaultpro.com/api/health +sudo systemctl status act_runner +sudo journalctl -u act_runner -f ``` -### Verify Stage Fails - -**Check external connectivity:** +**Permission denied:** ```bash -curl -sf https://motovaultpro.com/api/health -``` - -**Check container health:** -```bash -docker inspect --format='{{.State.Health.Status}}' mvp-backend-blue +sudo usermod -aG docker act_runner +sudo systemctl restart act_runner ``` --- ## Quick Reference +### Workflow Locations + +| Workflow | File | +|----------|------| +| Staging | `.gitea/workflows/staging.yaml` | +| Production | `.gitea/workflows/production.yaml` | +| Maintenance | `.gitea/workflows/maintenance.yaml` | +| Mirror Images | `.gitea/workflows/mirror-images.yaml` | + ### Important Paths | Path | Description | @@ -472,11 +377,11 @@ docker inspect --format='{{.State.Health.Status}}' mvp-backend-blue ### Common Commands ```bash -# View current state +# View deployment state cat config/deployment/state.json | jq . -# Check container status -docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Health}}" +# Check containers +docker ps --format "table {{.Names}}\t{{.Status}}" # View logs docker logs mvp-backend-blue -f @@ -486,21 +391,14 @@ docker logs mvp-backend-blue -f # Run health check ./scripts/ci/health-check.sh blue - -# Send test notification -./scripts/ci/notify.sh success "Test message" ``` -### Memory Budget (8GB Server) +### Email Notifications -| Component | RAM | -|-----------|-----| -| Blue frontend | 512MB | -| Blue backend | 1GB | -| Green frontend | 512MB | -| Green backend | 1GB | -| PostgreSQL | 2GB | -| Redis | 512MB | -| Traefik | 128MB | -| System | 1.3GB | -| **Total** | ~7GB | +| Event | Trigger | +|-------|---------| +| Staging Ready | Staging verified successfully | +| Success | Production deployed successfully | +| Failure | Deployment or verification failed | +| Rollback | Auto-rollback executed | +| Maintenance | Migration started/completed | diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 2dcd775..1a401cc 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,8 +1,8 @@ # Production Dockerfile for MotoVaultPro Frontend -# Uses mirrored base images from GitLab Container Registry +# Uses mirrored base images from Gitea Package Registry -# Build argument for registry (defaults to GitLab mirrors, falls back to Docker Hub) -ARG REGISTRY_MIRRORS=registry.motovaultpro.com/mirrors +# Build argument for registry (defaults to Gitea mirrors, falls back to Docker Hub) +ARG REGISTRY_MIRRORS=git.motovaultpro.com/egullickson/mirrors # Stage 1: Base with dependencies FROM ${REGISTRY_MIRRORS}/node:20-alpine AS base diff --git a/scripts/ci/mirror-base-images.sh b/scripts/ci/mirror-base-images.sh index 0a8c2ca..da2264a 100755 --- a/scripts/ci/mirror-base-images.sh +++ b/scripts/ci/mirror-base-images.sh @@ -5,7 +5,7 @@ set -euo pipefail -REGISTRY="${REGISTRY:-registry.motovaultpro.com/mirrors}" +REGISTRY="${REGISTRY:-git.motovaultpro.com/egullickson/mirrors}" # Base images required by MotoVaultPro IMAGES=( diff --git a/scripts/ci/notify.sh b/scripts/ci/notify.sh index 54d49f4..c568571 100755 --- a/scripts/ci/notify.sh +++ b/scripts/ci/notify.sh @@ -3,7 +3,7 @@ # Sends email notifications for deployment events # # Usage: ./notify.sh [message] [commit_sha] -# event_type: success, failure, rollback, rollback_failed, maintenance_start, maintenance_end +# event_type: success, failure, rollback, rollback_failed, maintenance_start, maintenance_end, staging_ready # message: Optional custom message # commit_sha: Optional commit SHA for context # @@ -39,15 +39,16 @@ if [[ -z "$NOTIFY_EMAIL" ]]; then exit 0 fi -# Get Resend API key -RESEND_API_KEY="" -if [[ -f "/run/secrets/resend-api-key" ]]; then - RESEND_API_KEY=$(cat /run/secrets/resend-api-key) -elif [[ -f "$PROJECT_ROOT/secrets/app/resend-api-key.txt" ]]; then - RESEND_API_KEY=$(cat "$PROJECT_ROOT/secrets/app/resend-api-key.txt") +# Get Resend API key (check env first for Gitea, then files for containers) +if [[ -z "${RESEND_API_KEY:-}" ]]; then + if [[ -f "/run/secrets/resend-api-key" ]]; then + RESEND_API_KEY=$(cat /run/secrets/resend-api-key) + elif [[ -f "$PROJECT_ROOT/secrets/app/resend-api-key.txt" ]]; then + RESEND_API_KEY=$(cat "$PROJECT_ROOT/secrets/app/resend-api-key.txt") + fi fi -if [[ -z "$RESEND_API_KEY" ]]; then +if [[ -z "${RESEND_API_KEY:-}" ]]; then echo "WARNING: Resend API key not found, skipping notification" exit 0 fi @@ -96,6 +97,13 @@ case "$EVENT_TYPE" in STATUS_TEXT="Maintenance Complete" DEFAULT_MESSAGE="Maintenance window complete. Application is online." ;; + "staging_ready") + SUBJECT="Staging Ready for Production - MotoVaultPro" + STATUS_COLOR="#3b82f6" + STATUS_EMOJI="[STAGING]" + STATUS_TEXT="Staging Verified" + DEFAULT_MESSAGE="Staging deployment verified. Ready for production deployment." + ;; *) echo "Unknown event type: $EVENT_TYPE" exit 1 diff --git a/scripts/inject-secrets.sh b/scripts/inject-secrets.sh index 86fb3cd..410fdf1 100755 --- a/scripts/inject-secrets.sh +++ b/scripts/inject-secrets.sh @@ -1,33 +1,26 @@ #!/bin/bash # inject-secrets.sh -# Writes GitLab CI File type variables to the secrets directory -# for K8s-style secret mounting in Docker Compose +# Writes secrets to the secrets directory for K8s-style secret mounting # -# GitLab File variables provide the PATH to a temporary file containing the secret. -# This script copies those files to the expected secrets/app/ location. +# Supports two modes: +# 1. GitLab CI: File variables provide PATH to temp file +# 2. Gitea Actions: Environment variables contain the secret value directly # -# IMPORTANT: In GitLab, variables MUST be set as "File" type, not "Variable" type. -# File type variables provide a PATH to a temp file containing the secret. -# Variable type provides the raw value, which will NOT work with this script. -# -# Required GitLab CI/CD Variables (File type): +# Required environment variables: # - POSTGRES_PASSWORD # - AUTH0_CLIENT_SECRET -# - AUTH0_MANAGEMENT_CLIENT_ID (Auth0 Management API client ID for user signup) -# - AUTH0_MANAGEMENT_CLIENT_SECRET (Auth0 Management API client secret) +# - AUTH0_MANAGEMENT_CLIENT_ID +# - AUTH0_MANAGEMENT_CLIENT_SECRET # - GOOGLE_MAPS_API_KEY # - GOOGLE_MAPS_MAP_ID -# - CF_DNS_API_TOKEN (Cloudflare DNS API token for Let's Encrypt certificates) -# - RESEND_API_KEY (Resend API key for email notifications) -# -# Required GitLab CI/CD Variables (Variable type): -# - DEPLOY_PATH +# - CF_DNS_API_TOKEN +# - RESEND_API_KEY set -euo pipefail # Configuration DEPLOY_PATH="${DEPLOY_PATH:-/opt/motovaultpro}" -SECRETS_DIR="${DEPLOY_PATH}/secrets/app" +SECRETS_DIR="${SECRETS_DIR:-${DEPLOY_PATH}/secrets/app}" # List of all secret files (must match docker-compose volume mounts) SECRET_FILES=( @@ -66,37 +59,33 @@ mkdir -p "$SECRETS_DIR" chmod 700 "$SECRETS_DIR" # Function to inject a secret +# Supports both: +# - Direct value (Gitea Actions): VAR contains the secret +# - File path (GitLab CI): VAR contains path to file with secret inject_secret() { local var_name="$1" local file_name="$2" local target_path="${SECRETS_DIR}/${file_name}" - # GitLab File variables contain the PATH to a temp file - local source_path="${!var_name:-}" + local source_value="${!var_name:-}" - if [ -z "$source_path" ]; then + if [ -z "$source_value" ]; then echo " ERROR: Variable $var_name is not set" - echo " Ensure it exists in GitLab CI/CD Variables" return 1 fi - # Check if it looks like a raw value instead of a file path - if [[ ! "$source_path" =~ ^/ ]]; then - echo " ERROR: $var_name appears to be a raw value, not a file path" - echo " In GitLab, change the variable Type from 'Variable' to 'File'" - return 1 + # Check if it's a file path (GitLab CI File variable) + if [[ "$source_value" =~ ^/ ]] && [ -f "$source_value" ]; then + # GitLab mode: copy from file + cp "$source_value" "$target_path" + echo " OK: $file_name (from file)" + else + # Gitea mode: write value directly + echo -n "$source_value" > "$target_path" + echo " OK: $file_name (from env)" fi - if [ ! -f "$source_path" ]; then - echo " ERROR: File not found for $var_name at $source_path" - echo " Ensure the variable is set as 'File' type in GitLab" - return 1 - fi - - # Copy the secret file (644 so container users can read) - cp "$source_path" "$target_path" chmod 644 "$target_path" - echo " OK: $file_name" } # Inject all secrets @@ -114,7 +103,6 @@ inject_secret "RESEND_API_KEY" "resend-api-key.txt" || FAILED=1 if [ $FAILED -eq 1 ]; then echo "" echo "ERROR: One or more secrets failed to inject" - echo "Ensure all required CI/CD variables are configured as File type in GitLab" exit 1 fi