306 lines
14 KiB
YAML
306 lines
14 KiB
YAML
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
|
||
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
|
||
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}\`"
|
||
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: 🔐 Registry Login
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: registry.infra.mintel.me
|
||
username: ${{ secrets.REGISTRY_USER }}
|
||
password: ${{ secrets.REGISTRY_PASS }}
|
||
|
||
- name: 🏗️ Docker Build & Push
|
||
uses: docker/build-push-action@v5
|
||
with:
|
||
context: .
|
||
platforms: linux/arm64
|
||
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=${{ secrets.NPM_TOKEN }}
|
||
tags: registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:${{ needs.prepare.outputs.image_tag }}
|
||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:buildcache
|
||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:buildcache,mode=max
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 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 "${{ 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"
|
||
EOF
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 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
|