name: Build & Deploy 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 }} cancel-in-progress: false 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 }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: 🔍 Environment & Version ermitteln id: determine run: | TAG="${{ github.ref_name }}" SHORT_SHA="${{ github.sha }}" SHORT_SHA="${SHORT_SHA:0: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 if [[ "$COMMIT_MSG" =~ ^chore: ]]; then TARGET="skip" GOTIFY_TITLE="ℹ️ Skip Deploy (Chore)" GOTIFY_PRIORITY=2 else 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 fi 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}" # Note: Host() backticks usually needed in compose 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" >> $GITHUB_OUTPUT echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "env_file=$ENV_FILE" >> $GITHUB_OUTPUT echo "traefik_host=$TRAEFIK_HOST" >> $GITHUB_OUTPUT echo "next_public_base_url=$NEXT_PUBLIC_BASE_URL" >> $GITHUB_OUTPUT echo "directus_url=$DIRECTUS_URL" >> $GITHUB_OUTPUT echo "directus_host=$DIRECTUS_HOST" >> $GITHUB_OUTPUT echo "project_name=$PROJECT_NAME" >> $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 # ────────────────────────────────────────────────────────────────────────────── # JOB 2: Quality Assurance (Lint & Test) # ────────────────────────────────────────────────────────────────────────────── qa: name: 🧪 QA needs: prepare if: needs.prepare.outputs.target != 'skip' runs-on: docker steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Install dependencies run: npm ci - name: 🧪 Run Checks if: github.event.inputs.skip_long_checks != 'true' run: | npm run lint npm run typecheck npm run test # ────────────────────────────────────────────────────────────────────────────── # JOB 3: Build & Push # ────────────────────────────────────────────────────────────────────────────── build: name: 🏗️ Build needs: prepare if: needs.prepare.outputs.target != 'skip' runs-on: docker steps: - name: Checkout repository uses: actions/checkout@v4 - name: 🔐 Registry Login run: | echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin - name: 🏗️ Docker Build & Push env: IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} run: | docker buildx build \ --pull \ --platform linux/arm64 \ --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ -t registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:$IMAGE_TAG \ --push . # ────────────────────────────────────────────────────────────────────────────── # JOB 4: Deploy # ────────────────────────────────────────────────────────────────────────────── deploy: name: 🚀 Deploy needs: [prepare, build, qa] if: needs.prepare.outputs.target != 'skip' runs-on: docker env: TARGET: ${{ needs.prepare.outputs.target }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: 🚀 Deploy via SSH env: IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} ENV_FILE: ${{ needs.prepare.outputs.env_file }} TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} 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=$TRAEFIK_HOST PROJECT_NAME=$PROJECT_NAME ENV_FILE=$ENV_FILE # App Config NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} # 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 }}" 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=168h" EOF # ────────────────────────────────────────────────────────────────────────────── # JOB 5: Notifications # ────────────────────────────────────────────────────────────────────────────── notifications: name: 🔔 Notifications needs: [prepare, deploy] if: always() runs-on: docker steps: - name: 🔔 Gotify 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 }}**" \ -F "priority=4" || true