name: Build & Deploy Mintel.me on: push: branches: - main tags: - 'v*' workflow_dispatch: inputs: skip_long_checks: description: 'Skip tests? (true/false)' required: false default: 'false' concurrency: group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_type == 'tag' && 'staging' || 'testing') }} cancel-in-progress: true jobs: # ────────────────────────────────────────────────────────────────────────────── # JOB 1: Prepare & Determine Environment # ────────────────────────────────────────────────────────────────────────────── prepare: name: 🔍 Prepare Environment 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 }} next_public_base_url: ${{ steps.determine.outputs.next_public_base_url }} directus_url: ${{ steps.determine.outputs.directus_url }} directus_host: ${{ steps.determine.outputs.directus_host }} project_name: ${{ steps.determine.outputs.project_name }} is_prod: ${{ steps.determine.outputs.is_prod }} gotify_title: ${{ steps.determine.outputs.gotify_title }} gotify_priority: ${{ steps.determine.outputs.gotify_priority }} short_sha: ${{ steps.determine.outputs.short_sha }} commit_msg: ${{ steps.determine.outputs.commit_msg }} container: image: catthehacker/ubuntu:act-latest steps: - name: 🧹 Maintenance (High Density Cleanup) shell: bash run: | echo "Purging old build layers and dangling images..." docker image prune -f docker builder prune -f --filter "until=6h" - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: 🔍 Environment & Version ermitteln id: determine shell: bash run: | TAG="${{ github.ref_name }}" SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-9) IMAGE_TAG="sha-${SHORT_SHA}" 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.mintel.me`' NEXT_PUBLIC_BASE_URL="https://testing.mintel.me" DIRECTUS_URL="https://cms.testing.mintel.me" DIRECTUS_HOST='`cms.testing.mintel.me`' PROJECT_NAME="mintel-me-testing" 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='`mintel.me`, `www.mintel.me`' NEXT_PUBLIC_BASE_URL="https://mintel.me" DIRECTUS_URL="https://cms.mintel.me" DIRECTUS_HOST='`cms.mintel.me`' PROJECT_NAME="mintel-me-prod" 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.mintel.me`' NEXT_PUBLIC_BASE_URL="https://staging.mintel.me" DIRECTUS_URL="https://cms.staging.mintel.me" DIRECTUS_HOST='`cms.staging.mintel.me`' PROJECT_NAME="mintel-me-staging" 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" echo "image_tag=$IMAGE_TAG" echo "env_file=$ENV_FILE" echo "traefik_host=$TRAEFIK_HOST" echo "next_public_base_url=$NEXT_PUBLIC_BASE_URL" echo "directus_url=$DIRECTUS_URL" echo "directus_host=$DIRECTUS_HOST" echo "project_name=$PROJECT_NAME" echo "is_prod=$IS_PROD" echo "gotify_title=$GOTIFY_TITLE" echo "gotify_priority=$GOTIFY_PRIORITY" echo "short_sha=$SHORT_SHA" echo "commit_msg=$COMMIT_MSG" } >> "$GITHUB_OUTPUT" # ────────────────────────────────────────────────────────────────────────────── # JOB 2: Quality Assurance (pnpm Lint & Test) # ────────────────────────────────────────────────────────────────────────────── qa: name: 🧪 Quality Assurance needs: prepare if: needs.prepare.outputs.target != 'skip' runs-on: docker container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: pnpm install --frozen-lockfile env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: 🧪 Run Checks in Parallel if: github.event.inputs.skip_long_checks != 'true' run: | pnpm lint & LINT_PID=$! pnpm --filter @mintel/web typecheck & TYPE_PID=$! # pnpm test & # TEST_PID=$! wait $LINT_PID || exit 1 wait $TYPE_PID || exit 1 # wait $TEST_PID || exit 1 # ────────────────────────────────────────────────────────────────────────────── # JOB 3: Build & Push Docker Image # ────────────────────────────────────────────────────────────────────────────── build: name: 🏗️ Build App needs: prepare if: ${{ needs.prepare.outputs.target != 'skip' }} runs-on: docker container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: 🐳 Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: 🔐 Registry Login run: | echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin - name: 🏗️ App bauen & pushen env: IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} TARGET: ${{ needs.prepare.outputs.target }} NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: | docker buildx build \ --pull \ --platform linux/arm64 \ --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ --build-arg NEXT_PUBLIC_TARGET="$TARGET" \ --build-arg DIRECTUS_URL="$DIRECTUS_URL" \ --secret id=NPM_TOKEN,env=NPM_TOKEN \ -t registry.infra.mintel.me/mintel/mintel.me:$IMAGE_TAG \ --cache-from type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache \ --cache-to type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache,mode=max \ --push . # ────────────────────────────────────────────────────────────────────────────── # JOB 4: Deploy via SSH # ────────────────────────────────────────────────────────────────────────────── deploy: name: 🚀 Deploy needs: [prepare, build, qa] if: ${{ needs.prepare.outputs.target != 'skip' }} runs-on: docker container: image: catthehacker/ubuntu:act-latest env: TARGET: ${{ needs.prepare.outputs.target }} IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} ENV_FILE: ${{ needs.prepare.outputs.env_file }} PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: 🚀 Deploy via SSH shell: bash run: | mkdir -p ~/.ssh echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null # Create .env on the fly cat > /tmp/mintel.me.env << EOF # Generated by CI - $TARGET - $(date -u) NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} NEXT_PUBLIC_TARGET=$TARGET DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} DIRECTUS_HOST=${{ needs.prepare.outputs.directus_host }} TRAEFIK_HOST=${{ needs.prepare.outputs.traefik_host }} IMAGE_TAG=$IMAGE_TAG PROJECT_NAME=$PROJECT_NAME AUTH_MIDDLEWARE=$( [[ "$TARGET" == "production" ]] && echo "compress" || echo "${PROJECT_NAME}-auth,compress" ) # Secrets DIRECTUS_KEY=${{ secrets.DIRECTUS_KEY }} DIRECTUS_SECRET=${{ secrets.DIRECTUS_SECRET }} DIRECTUS_DB_NAME=${{ secrets.DIRECTUS_DB_NAME || 'directus' }} DIRECTUS_DB_USER=${{ secrets.DIRECTUS_DB_USER || 'directus' }} DIRECTUS_DB_PASSWORD=${{ secrets.DIRECTUS_DB_PASSWORD }} # General NODE_ENV=production EOF # 1. Cleanup and Create Directories on server BEFORE SCP ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << 'EOF' set -e mkdir -p /home/deploy/sites/mintel.me/varnish mkdir -p /home/deploy/sites/mintel.me/directus/uploads /home/deploy/sites/mintel.me/directus/extensions if [ -d "/home/deploy/sites/mintel.me/varnish/default.vcl" ]; then echo "🧹 Removing directory 'varnish/default.vcl' created by Docker..." rm -rf /home/deploy/sites/mintel.me/varnish/default.vcl fi chown -R deploy:deploy /home/deploy/sites/mintel.me/directus /home/deploy/sites/mintel.me/varnish EOF # 2. Transfer files scp /tmp/mintel.me.env root@alpha.mintel.me:/home/deploy/sites/mintel.me/$ENV_FILE scp docker-compose.yml root@alpha.mintel.me:/home/deploy/sites/mintel.me/docker-compose.yml scp -r varnish root@alpha.mintel.me:/home/deploy/sites/mintel.me/ ssh root@alpha.mintel.me IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" PROJECT_NAME="$PROJECT_NAME" bash << 'EOF' set -e cd /home/deploy/sites/mintel.me echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --remove-orphans docker system prune -f --filter "until=24h" echo "→ Waiting 15s for warmup..." sleep 15 docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" ps EOF # ────────────────────────────────────────────────────────────────────────────── # JOB 5: PageSpeed Test # ────────────────────────────────────────────────────────────────────────────── pagespeed: name: ⚡ PageSpeed needs: [prepare, deploy] if: | always() && needs.prepare.outputs.target != 'skip' && needs.deploy.result == 'success' && github.event.inputs.skip_long_checks != 'true' runs-on: docker container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: pnpm install --frozen-lockfile env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: 🔍 Install Chromium (ARM64) run: | apt-get update apt-get install -y gnupg wget ca-certificates CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME) mkdir -p /etc/apt/keyrings KEY_ID="82BB6851C64F6880" wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb apt-get update apt-get install -y --allow-downgrades chromium [ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome continue-on-error: true - name: 🧪 Run PageSpeed (Lighthouse) env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} PAGESPEED_LIMIT: 8 CHROME_PATH: /usr/bin/chromium run: pnpm --filter @mintel/web run pagespeed:test # ────────────────────────────────────────────────────────────────────────────── # JOB 6: Notifications # ────────────────────────────────────────────────────────────────────────────── notifications: name: 🔔 Notifications needs: [prepare, qa, build, deploy, pagespeed] if: always() runs-on: docker container: image: catthehacker/ubuntu:act-latest steps: - name: 🔔 Gotify run: | STATUS="${{ needs.deploy.result == 'success' && '✅' || '❌' }}" PRIORITY="${{ needs.deploy.result == 'success' && needs.prepare.outputs.gotify_priority || '8' }}" curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=$STATUS ${{ needs.prepare.outputs.gotify_title }}" \ -F "message=Deploy to **${{ needs.prepare.outputs.target }}** ${{ needs.deploy.result }}.\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }} (${{ needs.prepare.outputs.commit_msg }})\nActor: ${{ github.actor }}" \ -F "priority=$PRIORITY" || true