name: Build & Deploy on: push: branches: - main tags: - '*' workflow_dispatch: inputs: skip_long_checks: description: 'Skip tests? (true/false)' required: false default: 'false' concurrency: group: ${{ github.workflow }} cancel-in-progress: false jobs: # ────────────────────────────────────────────────────────────────────────────── # JOB 1: Prepare & Determine Environment # ────────────────────────────────────────────────────────────────────────────── prepare: name: 🔍 Prepare Environment runs-on: docker container: image: catthehacker/ubuntu:act-latest 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 }} 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: 2 - name: 🔍 Environment & Version ermitteln id: determine run: | TAG="${{ github.ref_name }}" SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-9) COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available") # Base Domain (e.g. example.com) DOMAIN_BASE=$(echo "${{ secrets.NEXT_PUBLIC_BASE_URL }}" | sed -E 's|https?://||' | sed -E 's|/.*||') PRJ_ID="${{ github.event.repository.name }}" if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then TARGET="testing" IMAGE_TAG="main-${SHORT_SHA}" ENV_FILE=".env.testing" TRAEFIK_HOST="\`testing.\${DOMAIN_BASE}\`" NEXT_PUBLIC_BASE_URL="https://testing.\${DOMAIN_BASE}" DIRECTUS_URL="https://cms.testing.\${DOMAIN_BASE}" DIRECTUS_HOST="\`cms.testing.\${DOMAIN_BASE}\`" PROJECT_NAME="\${PRJ_ID}-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="\`\${DOMAIN_BASE}\`, \`www.\${DOMAIN_BASE}\`" NEXT_PUBLIC_BASE_URL="https://\${DOMAIN_BASE}" DIRECTUS_URL="https://cms.\${DOMAIN_BASE}" DIRECTUS_HOST="\`cms.\${DOMAIN_BASE}\`" PROJECT_NAME="\${PRJ_ID}-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.\${DOMAIN_BASE}\`" NEXT_PUBLIC_BASE_URL="https://staging.\${DOMAIN_BASE}" DIRECTUS_URL="https://cms.staging.\${DOMAIN_BASE}" DIRECTUS_HOST="\`cms.staging.\${DOMAIN_BASE}\`" PROJECT_NAME="\${PRJ_ID}-staging" IS_PROD="false" GOTIFY_TITLE="🧪 Staging-Deploy (Pre-Release)" GOTIFY_PRIORITY=5 else TARGET="skip" 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 (Lint & Test) # ────────────────────────────────────────────────────────────────────────────── qa: name: 🧪 QA 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 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: 🧪 Run Checks in Parallel if: github.event.inputs.skip_long_checks != 'true' run: | npm run lint & LINT_PID=$! npm run typecheck & TYPE_PID=$! npm run test & TEST_PID=$! wait $LINT_PID || exit 1 wait $TYPE_PID || exit 1 wait $TEST_PID || exit 1 # ────────────────────────────────────────────────────────────────────────────── # JOB 3: Build & Push # ────────────────────────────────────────────────────────────────────────────── build: name: 🏗️ Build 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 - name: 🐳 Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: 🔐 Discover Valid Registry Token id: discover_token run: | echo "Testing available secrets against git.infra.mintel.me Docker registry..." TOKENS="${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}" USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel" for TOKEN in $TOKENS; do if [ -n "$TOKEN" ]; then for U in $USERS; do if [ -n "$U" ]; then echo "Attempting docker login for a token with user $U..." if echo "$TOKEN" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then echo "✅ Successfully authenticated with a token." echo "::add-mask::$TOKEN" echo "token=$TOKEN" >> $GITHUB_OUTPUT echo "user=$U" >> $GITHUB_OUTPUT exit 0 fi fi done fi done echo "❌ All available tokens failed to authenticate!" exit 1 - name: 🏗️ Docker Build & Push uses: docker/build-push-action@v5 with: context: . file: packages/infra/docker/Dockerfile.nextjs platforms: linux/amd64 pull: true provenance: false build-args: | NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} push: true secrets: | NPM_TOKEN=${{ steps.discover_token.outputs.token }} tags: git.infra.mintel.me/mmintel/${{ github.event.repository.name }}:${{ needs.prepare.outputs.image_tag }} # ────────────────────────────────────────────────────────────────────────────── # JOB 4: Deploy # ────────────────────────────────────────────────────────────────────────────── 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 }} PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} ENV_FILE: ${{ needs.prepare.outputs.env_file }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: 🚀 Deploy via SSH run: | 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 # Generate .env from secrets cat > /tmp/app.env << EOF # Generated by CI - $TARGET - $(date -u) NODE_ENV=production IMAGE_TAG=$IMAGE_TAG TRAEFIK_HOST=${{ needs.prepare.outputs.traefik_host }} PROJECT_NAME=$PROJECT_NAME ENV_FILE=$ENV_FILE # App Config NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} NEXT_PUBLIC_TARGET=$TARGET # Directus Config DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} DIRECTUS_HOST=${{ needs.prepare.outputs.directus_host }} DIRECTUS_KEY=${{ secrets.DIRECTUS_KEY }} DIRECTUS_SECRET=${{ secrets.DIRECTUS_SECRET }} DIRECTUS_ADMIN_EMAIL=${{ secrets.DIRECTUS_ADMIN_EMAIL }} DIRECTUS_ADMIN_PASSWORD=${{ secrets.DIRECTUS_ADMIN_PASSWORD }} DIRECTUS_DB_PASSWORD=${{ secrets.DIRECTUS_DB_PASSWORD }} # Gatekeeper GATEKEEPER_PASSWORD=${{ secrets.GATEKEEPER_PASSWORD || 'mintel' }} AUTH_MIDDLEWARE=$( [[ "$TARGET" == "production" ]] && echo "compress" || echo "$PROJECT_NAME-auth,compress" ) EOF APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}" ssh root@${{ secrets.SSH_HOST }} "mkdir -p $APP_DIR" scp /tmp/app.env root@${{ secrets.SSH_HOST }}:$APP_DIR/$ENV_FILE scp docker-compose.yml root@${{ secrets.SSH_HOST }}:$APP_DIR/docker-compose.yml ssh root@${{ secrets.SSH_HOST }} IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" PROJECT_NAME="$PROJECT_NAME" bash << 'EOF' set -e cd "/home/deploy/sites/${{ github.event.repository.name }}" chmod 600 "$ENV_FILE" echo "${{ steps.discover_token.outputs.token }}" | docker login git.infra.mintel.me -u "${{ steps.discover_token.outputs.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" EOF - name: 🧹 Post-Deploy Cleanup (Runner) if: always() run: docker builder prune -f --filter "until=1h" # ────────────────────────────────────────────────────────────────────────────── # JOB 5: Notifications # ────────────────────────────────────────────────────────────────────────────── notifications: name: 🔔 Notifications needs: [prepare, deploy] if: always() runs-on: docker container: image: catthehacker/ubuntu:act-latest steps: - name: 🔔 Gotify - Success if: needs.deploy.result == 'success' run: | curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=${{ needs.prepare.outputs.gotify_title }}" \ -F "message=Erfolgreich deployt auf **${{ needs.prepare.outputs.target }}**\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }}\nRun: ${{ github.run_id }}" \ -F "priority=4" || true - name: 🔔 Gotify - Failure if: | needs.prepare.result == 'failure' || needs.qa.result == 'failure' || needs.build.result == 'failure' || needs.deploy.result == 'failure' run: | curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=❌ Deployment FEHLGESCHLAGEN – ${{ github.event.repository.name }}" \ -F "message=**Fehler beim Deploy auf ${{ needs.prepare.outputs.target || 'unknown' }}**\n\nRun: ${{ github.run_id }}\nBitte Logs prüfen!" \ -F "priority=8" || true