name: Build & Deploy on: push: branches: - main tags: - 'v*' jobs: build-and-deploy: runs-on: docker steps: # ────────────────────────────────────────────────────────────────────────────── # Workflow Start & Basic Info # ────────────────────────────────────────────────────────────────────────────── - name: 📢 Workflow Start run: | echo "┌──────────────────────────────────────────────────────────────┐" echo "│ 🚀 Deployment Workflow gestartet │" echo "├──────────────────────────────────────────────────────────────┤" echo "│ Repository: ${{ github.repository }} │" echo "│ Ref: ${{ github.ref }} │" echo "│ Ref-Name: ${{ github.ref_name }} │" echo "│ Commit: ${{ github.sha }} │" echo "│ Actor: ${{ github.actor }} │" echo "│ Datum: $(date -u +'%Y-%m-%d %H:%M:%S UTC') │" echo "└──────────────────────────────────────────────────────────────┘" - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 # ────────────────────────────────────────────────────────────────────────────── # Environment bestimmen + Commit-Message holen # ────────────────────────────────────────────────────────────────────────────── - name: 🔍 Environment & Version ermitteln id: determine run: | TAG="${{ github.ref_name }}" SHORT_SHA="${{ github.sha }}" SHORT_SHA="${SHORT_SHA:0:9}" # Get base domain from secret or env if possible, otherwise placeholder # In a real project, you'd likely have a primary domain secret DOMAIN_BASE=$(echo "${{ secrets.NEXT_PUBLIC_BASE_URL }}" | sed -E 's|https?://||' | sed -E 's|/.*||') # Commit-Message holen (erste Zeile) COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available") if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then TARGET="testing" IMAGE_TAG="main-${SHORT_SHA}" ENV_FILE=".env.testing" TRAEFIK_HOST="\`testing.${DOMAIN_BASE}\`" IS_PROD="false" GOTIFY_TITLE="🧪 Testing-Deploy" GOTIFY_PRIORITY=4 elif [[ "${{ github.ref_type }}" == "tag" ]]; then if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then TARGET="production" IMAGE_TAG="$TAG" ENV_FILE=".env.prod" TRAEFIK_HOST="\`${DOMAIN_BASE}\`, \`www.${DOMAIN_BASE}\`" IS_PROD="true" GOTIFY_TITLE="🚀 Production-Release" GOTIFY_PRIORITY=6 elif [[ "$TAG" =~ -rc\. || "$TAG" =~ -beta\. || "$TAG" =~ -alpha\. ]]; then TARGET="staging" IMAGE_TAG="$TAG" ENV_FILE=".env.staging" TRAEFIK_HOST="\`staging.${DOMAIN_BASE}\`" IS_PROD="false" GOTIFY_TITLE="🧪 Staging-Deploy (Pre-Release)" GOTIFY_PRIORITY=5 else TARGET="skip" GOTIFY_TITLE="❓ Unbekannter Tag" GOTIFY_PRIORITY=3 fi else TARGET="skip" fi echo "target=$TARGET" >> $GITHUB_OUTPUT echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "env_file=$ENV_FILE" >> $GITHUB_OUTPUT echo "traefik_host=$TRAEFIK_HOST" >> $GITHUB_OUTPUT echo "is_prod=$IS_PROD" >> $GITHUB_OUTPUT echo "gotify_title=$GOTIFY_TITLE" >> $GITHUB_OUTPUT echo "gotify_priority=$GOTIFY_PRIORITY" >> $GITHUB_OUTPUT echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT - name: ⏭️ Skip Deployment if: steps.determine.outputs.target == 'skip' run: | echo "Deployment übersprungen – kein passender Trigger (main oder v*-Tag)" exit 0 # ────────────────────────────────────────────────────────────────────────────── # Registry Login # ────────────────────────────────────────────────────────────────────────────── - name: 🔐 Registry Login run: | echo "🔐 Login zu registry.infra.mintel.me ..." echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin # ────────────────────────────────────────────────────────────────────────────── # Build & Push # ────────────────────────────────────────────────────────────────────────────── - name: 🏗️ Docker Image bauen & pushen env: IMAGE_TAG: ${{ steps.determine.outputs.image_tag }} NEXT_PUBLIC_BASE_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} run: | echo "🏗️ Building → ${{ steps.determine.outputs.target }} / $IMAGE_TAG" docker buildx build \ --pull \ --platform linux/arm64 \ --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \ -t registry.infra.mintel.me/${{ github.repository }}:$IMAGE_TAG \ --push . # ────────────────────────────────────────────────────────────────────────────── # Deploy via SSH # ────────────────────────────────────────────────────────────────────────────── - name: 🚀 Deploy to ${{ steps.determine.outputs.target }} env: IMAGE_TAG: ${{ steps.determine.outputs.image_tag }} ENV_FILE: ${{ steps.determine.outputs.env_file }} TRAEFIK_HOST: ${{ steps.determine.outputs.traefik_host }} NEXT_PUBLIC_BASE_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} SENTRY_DSN: ${{ steps.determine.outputs.target == 'production' && secrets.SENTRY_DSN || (steps.determine.outputs.target == 'staging' && secrets.STAGING_SENTRY_DSN || secrets.TESTING_SENTRY_DSN || secrets.SENTRY_DSN) }} MAIL_HOST: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_HOST || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_HOST || secrets.TESTING_MAIL_HOST || secrets.MAIL_HOST) }} MAIL_PORT: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_PORT || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_PORT || secrets.TESTING_MAIL_PORT || secrets.MAIL_PORT) }} MAIL_USERNAME: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_USERNAME || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_USERNAME || secrets.TESTING_MAIL_USERNAME || secrets.MAIL_USERNAME) }} MAIL_PASSWORD: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_PASSWORD || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_PASSWORD || secrets.TESTING_MAIL_PASSWORD || secrets.MAIL_PASSWORD) }} MAIL_FROM: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_FROM || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_FROM || secrets.TESTING_MAIL_FROM || secrets.MAIL_FROM) }} MAIL_RECIPIENTS: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_RECIPIENTS || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_RECIPIENTS || secrets.TESTING_MAIL_RECIPIENTS || secrets.MAIL_RECIPIENTS) }} run: | echo "Deploying ${{ steps.determine.outputs.target }} → $IMAGE_TAG" # SSH vorbereiten mkdir -p ~/.ssh echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null # .env-Datei erstellen cat > /tmp/app.env << EOF # Generated by CI - ${{ steps.determine.outputs.target }} - $(date -u) NODE_ENV=production NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL SENTRY_DSN=$SENTRY_DSN MAIL_HOST=$MAIL_HOST MAIL_PORT=$MAIL_PORT MAIL_USERNAME=$MAIL_USERNAME MAIL_PASSWORD=$MAIL_PASSWORD MAIL_FROM=$MAIL_FROM MAIL_RECIPIENTS=$MAIL_RECIPIENTS IMAGE_TAG=$IMAGE_TAG TRAEFIK_HOST=$TRAEFIK_HOST ENV_FILE=$ENV_FILE EOF APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}" ssh -o StrictHostKeyChecking=accept-new root@${{ secrets.SSH_HOST }} "mkdir -p $APP_DIR" scp -o StrictHostKeyChecking=accept-new /tmp/app.env root@${{ secrets.SSH_HOST }}:$APP_DIR/$ENV_FILE scp -o StrictHostKeyChecking=accept-new docker-compose.yml root@${{ secrets.SSH_HOST }}:$APP_DIR/docker-compose.yml ssh -o StrictHostKeyChecking=accept-new root@${{ secrets.SSH_HOST }} bash << 'EOF' set -e APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}" cd $APP_DIR chmod 600 $ENV_FILE chown deploy:deploy $ENV_FILE echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin echo "→ Pulling image: $IMAGE_TAG" IMAGE_TAG=$IMAGE_TAG ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE pull echo "→ Starting containers..." IMAGE_TAG=$IMAGE_TAG ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE up -d docker system prune -f --filter "until=168h" echo "→ Waiting 15s for warmup..." sleep 15 echo "→ Container status:" docker compose --env-file $ENV_FILE ps if ! docker compose --env-file $ENV_FILE ps | grep -q "Up"; then echo "❌ Fehler: Container nicht Up!" docker compose --env-file $ENV_FILE logs --tail=150 exit 1 fi echo "✅ Deployment erfolgreich auf ${{ steps.determine.outputs.target }}!" EOF rm -f /tmp/app.env # ────────────────────────────────────────────────────────────────────────────── # Summary & Gotify # ────────────────────────────────────────────────────────────────────────────── - name: 📊 Deployment Summary if: always() run: | echo "┌──────────────────────────────┐" echo "│ Deployment Summary │" echo "├──────────────────────────────┤" echo "│ Status: ${{ job.status }} │" echo "│ Umgebung: ${{ steps.determine.outputs.target || 'skipped' }} │" echo "│ Version: ${{ steps.determine.outputs.image_tag }} │" echo "│ Commit: ${{ steps.determine.outputs.short_sha }} │" echo "│ Message: ${{ steps.determine.outputs.commit_msg }} │" echo "└──────────────────────────────┘" - name: 🔔 Gotify - Success if: success() run: | curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=${{ steps.determine.outputs.gotify_title }}" \ -F "message=Erfolgreich deployt auf **${{ steps.determine.outputs.target }}**\n\nVersion: **${{ steps.determine.outputs.image_tag }}**\nCommit: ${{ steps.determine.outputs.short_sha }} (${{ steps.determine.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}" \ -F "priority=${{ steps.determine.outputs.gotify_priority }}" || true - name: 🔔 Gotify - Failure if: failure() run: | curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=❌ Deployment FEHLGESCHLAGEN – ${{ steps.determine.outputs.target || 'unknown' }}" \ -F "message=**Fehler beim Deploy auf ${{ steps.determine.outputs.target }}**\n\nVersion: ${{ steps.determine.outputs.image_tag || '?' }}\nCommit: ${{ steps.determine.outputs.short_sha || '?' }}\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}\n\nBitte Logs prüfen!" \ -F "priority=8" || true