495 lines
19 KiB
YAML
495 lines
19 KiB
YAML
name: Build, Push and Deploy Florale Emotion Website
|
|
|
|
on:
|
|
push:
|
|
branches: [ 'feature/*', 'main', 'master' ]
|
|
pull_request:
|
|
branches: [ main, master ]
|
|
|
|
env:
|
|
HARBOR_REGISTRY: registry.julianvollmer.de
|
|
PROJECT_NAME: florale-emotion
|
|
|
|
jobs:
|
|
feature-branch:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
if: startsWith(github.ref, 'refs/heads/feature/')
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v3
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v2
|
|
with:
|
|
driver-opts: |
|
|
image=moby/buildkit:v0.12.0
|
|
buildkitd-flags: --debug
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
run: |
|
|
BRANCH_CLEAN=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9._-]/-/g')
|
|
SHORT_SHA="${{ github.sha }}"
|
|
SHORT_SHA="${SHORT_SHA:0:8}"
|
|
TAG="${BRANCH_CLEAN}-${SHORT_SHA}"
|
|
|
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
echo "frontend_image=${{ env.HARBOR_REGISTRY }}/${{ env.PROJECT_NAME }}/florale-emotion-frontend:${TAG}" >> $GITHUB_OUTPUT
|
|
echo "backend_image=${{ env.HARBOR_REGISTRY }}/${{ env.PROJECT_NAME }}/florale-emotion-backend:${TAG}" >> $GITHUB_OUTPUT
|
|
echo "bot_image=${{ env.HARBOR_REGISTRY }}/${{ env.PROJECT_NAME }}/florale-emotion-bot:${TAG}" >> $GITHUB_OUTPUT
|
|
|
|
- name: Login to Harbor Registry
|
|
run: |
|
|
echo "Harbor12345" | docker login ${{ env.HARBOR_REGISTRY }} -u admin --password-stdin
|
|
|
|
- name: Build and Push Frontend (Feature)
|
|
working-directory: ./website
|
|
run: |
|
|
IMAGE_NAME=${{ steps.meta.outputs.frontend_image }}
|
|
for attempt in 1 2 3; do
|
|
echo "Build attempt $attempt for feature frontend..."
|
|
docker build --no-cache --progress=plain -t "${IMAGE_NAME}" . || {
|
|
echo "Build failed on attempt $attempt"
|
|
if [ $attempt -eq 3 ]; then
|
|
echo "All build attempts failed"
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
continue
|
|
}
|
|
for push_attempt in 1 2 3; do
|
|
echo "Push attempt $push_attempt for feature frontend..."
|
|
docker push "${IMAGE_NAME}" && {
|
|
echo "Feature frontend push successful on attempt $push_attempt"
|
|
break
|
|
} || {
|
|
echo "Push failed on attempt $push_attempt"
|
|
if [ $push_attempt -eq 3 ]; then
|
|
echo "All push attempts failed"
|
|
exit 1
|
|
fi
|
|
sleep 30
|
|
}
|
|
done
|
|
break
|
|
done
|
|
|
|
echo "✅ Feature frontend build and push completed successfully!"
|
|
echo "🏷️ Tag: ${TAG}"
|
|
echo "📦 Image: ${IMAGE_NAME}"
|
|
|
|
- name: Build and Push Backend (Feature)
|
|
working-directory: ./website/backend
|
|
env:
|
|
IMAGE_NAME: ${{ steps.meta.outputs.backend_image }}
|
|
TAG: ${{ steps.meta.outputs.tag }}
|
|
run: |
|
|
echo "Building backend image: ${IMAGE_NAME}"
|
|
docker build --no-cache --progress=plain -t "${IMAGE_NAME}" .
|
|
echo "Pushing backend image: ${IMAGE_NAME}"
|
|
docker push "${IMAGE_NAME}"
|
|
echo "✅ Feature backend build and push completed successfully!"
|
|
echo "🏷️ Tag: ${TAG}"
|
|
echo "📦 Image: ${IMAGE_NAME}"
|
|
|
|
- name: Build and Push Social Media Bot (Feature)
|
|
working-directory: ./website/social-media-bot
|
|
env:
|
|
IMAGE_NAME: ${{ steps.meta.outputs.bot_image }}
|
|
TAG: ${{ steps.meta.outputs.tag }}
|
|
run: |
|
|
echo "Building social media bot image: ${IMAGE_NAME}"
|
|
docker build --no-cache --progress=plain -t "${IMAGE_NAME}" .
|
|
echo "Pushing social media bot image: ${IMAGE_NAME}"
|
|
docker push "${IMAGE_NAME}"
|
|
echo "✅ Feature social media bot build and push completed successfully!"
|
|
echo "🏷️ Tag: ${TAG}"
|
|
echo "📦 Image: ${IMAGE_NAME}"
|
|
|
|
- name: Setup kubectl
|
|
run: |
|
|
# Install kubectl directly to avoid permission issues
|
|
echo "Installing kubectl..."
|
|
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
|
chmod +x kubectl
|
|
sudo mv kubectl /usr/local/bin/
|
|
kubectl version --client
|
|
|
|
- name: Configure kubectl
|
|
env:
|
|
KUBECTLSECRET: ${{ secrets.KUBECTLSECRET }}
|
|
run: |
|
|
mkdir -p ~/.kube
|
|
|
|
echo "🔍 Debugging KUBECTLSECRET..."
|
|
echo "Secret length: ${#KUBECTLSECRET}"
|
|
|
|
# Check if secret is empty
|
|
if [ "${#KUBECTLSECRET}" -eq 0 ]; then
|
|
echo "❌ ERROR: KUBECTLSECRET is empty!"
|
|
exit 1
|
|
fi
|
|
|
|
# Try to decode as base64 first, if that fails, use as plain text
|
|
if echo "$KUBECTLSECRET" | base64 -d > ~/.kube/config 2>/dev/null; then
|
|
echo "✅ KUBECTLSECRET decoded as base64"
|
|
else
|
|
echo "⚠️ KUBECTLSECRET is not base64, using as plain text"
|
|
echo "$KUBECTLSECRET" > ~/.kube/config
|
|
fi
|
|
|
|
echo "📁 kubeconfig created at ~/.kube/config"
|
|
chmod 600 ~/.kube/config
|
|
|
|
# Debug kubeconfig content (without sensitive data)
|
|
echo "🔍 Debugging kubeconfig structure..."
|
|
echo "File size: $(wc -c < ~/.kube/config) bytes"
|
|
|
|
echo "First few lines of kubeconfig (structure check):"
|
|
head -20 ~/.kube/config | grep -E "(apiVersion|kind|clusters|contexts|users|current-context)" || echo "No standard kubeconfig structure found"
|
|
|
|
echo "Checking for current-context:"
|
|
grep "current-context:" ~/.kube/config || echo "❌ No current-context found"
|
|
|
|
echo "Checking for clusters:"
|
|
grep -A 2 "clusters:" ~/.kube/config || echo "❌ No clusters found"
|
|
|
|
echo "Checking for users:"
|
|
grep -A 2 "users:" ~/.kube/config || echo "❌ No users found"
|
|
|
|
# Fix TLS issues by adding insecure-skip-tls-verify to all clusters
|
|
echo "🔧 Fixing TLS verification for self-signed certificates..."
|
|
|
|
# Get all cluster names and add insecure-skip-tls-verify
|
|
kubectl config get-clusters | tail -n +2 | while read cluster; do
|
|
if [ -n "$cluster" ]; then
|
|
echo "Setting insecure-skip-tls-verify for cluster: $cluster"
|
|
kubectl config set-cluster "$cluster" --insecure-skip-tls-verify=true
|
|
fi
|
|
done
|
|
|
|
echo "✅ TLS configuration completed"
|
|
|
|
- name: Debug kubeconfig before kubectl test
|
|
run: |
|
|
echo "🔍 Final kubeconfig debug before kubectl test..."
|
|
echo "File exists: $(test -f ~/.kube/config && echo 'YES' || echo 'NO')"
|
|
echo "File size: $(wc -c < ~/.kube/config 2>/dev/null || echo '0') bytes"
|
|
|
|
if [ -f ~/.kube/config ]; then
|
|
echo "First 15 lines of kubeconfig:"
|
|
head -15 ~/.kube/config
|
|
echo "---"
|
|
echo "Contains 'insecure-skip-tls-verify'?: $(grep -c 'insecure-skip-tls-verify' ~/.kube/config || echo '0')"
|
|
echo "Contains 'client-certificate-data'?: $(grep -c 'client-certificate-data' ~/.kube/config || echo '0')"
|
|
echo "Contains 'client-key-data'?: $(grep -c 'client-key-data' ~/.kube/config || echo '0')"
|
|
echo "Current context: $(grep 'current-context:' ~/.kube/config || echo 'NOT FOUND')"
|
|
else
|
|
echo "❌ kubeconfig file does not exist!"
|
|
fi
|
|
|
|
- name: Test kubectl connection
|
|
run: |
|
|
kubectl version --client
|
|
echo "Testing cluster connection..."
|
|
kubectl get nodes
|
|
|
|
- name: Deploy Feature Branch
|
|
env:
|
|
FRONTEND_IMAGE: ${{ steps.meta.outputs.frontend_image }}
|
|
BACKEND_IMAGE: ${{ steps.meta.outputs.backend_image }}
|
|
BOT_IMAGE: ${{ steps.meta.outputs.bot_image }}
|
|
BRANCH_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
echo "🚀 Deploying feature branch: $BRANCH_NAME"
|
|
echo "Frontend image: $FRONTEND_IMAGE"
|
|
echo "Backend image: $BACKEND_IMAGE"
|
|
echo "Bot image: $BOT_IMAGE"
|
|
|
|
# Ensure namespace exists
|
|
kubectl create namespace florale-emotion --dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# Ensure Harbor registry secret exists
|
|
kubectl create secret docker-registry harbor-registry-secret \
|
|
--docker-server=${{ env.HARBOR_REGISTRY }} \
|
|
--docker-username=admin \
|
|
--docker-password=Harbor12345 \
|
|
--namespace=florale-emotion \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# Apply frontend deployment und ersetze das Image-Tag dynamisch (kein latest in Features)
|
|
sed "s|__IMAGE_TAG__|${{ steps.meta.outputs.tag }}|g" website/k8s/frontend.yaml | kubectl apply -n florale-emotion -f -
|
|
|
|
# Apply backend deployment mit Tag-Ersetzung
|
|
sed "s|__IMAGE_TAG__|${{ steps.meta.outputs.tag }}|g" website/k8s/backend.yaml | kubectl apply -n florale-emotion -f -
|
|
|
|
# Apply social media bot deployment mit Tag-Ersetzung
|
|
sed "s|__IMAGE_TAG__|${{ steps.meta.outputs.tag }}|g" website/k8s/bot.yaml | kubectl apply -n florale-emotion -f -
|
|
|
|
# Apply feature branch ingress (using dev subdomain)
|
|
kubectl apply -f website/k8s/ingress-dev.yaml -n florale-emotion
|
|
|
|
# Wait for rollout
|
|
echo "⏳ Waiting for rollout to finish..."
|
|
kubectl rollout status deployment/florale-emotion-frontend -n florale-emotion --timeout=300s
|
|
|
|
echo "✅ Feature branch deployment complete!"
|
|
echo "🌐 Dev URL: https://dev.florale-emotion.de"
|
|
echo "📦 Harbor Registry: https://registry.julianvollmer.de"
|
|
|
|
production-branch:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v3
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v2
|
|
with:
|
|
driver-opts: |
|
|
image=moby/buildkit:v0.12.0
|
|
buildkitd-flags: --debug
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
run: |
|
|
BRANCH_CLEAN=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9._-]/-/g')
|
|
SHORT_SHA="${{ github.sha }}"
|
|
SHORT_SHA="${SHORT_SHA:0:8}"
|
|
TAG="${BRANCH_CLEAN}-${SHORT_SHA}"
|
|
|
|
# Für Master-Branches zusätzlich latest Tag erstellen
|
|
echo "Master branch detected, will create latest tags"
|
|
|
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
echo "frontend_image=${{ env.HARBOR_REGISTRY }}/${{ env.PROJECT_NAME }}/florale-emotion-frontend:${TAG}" >> $GITHUB_OUTPUT
|
|
echo "backend_image=${{ env.HARBOR_REGISTRY }}/${{ env.PROJECT_NAME }}/florale-emotion-backend:${TAG}" >> $GITHUB_OUTPUT
|
|
echo "bot_image=${{ env.HARBOR_REGISTRY }}/${{ env.PROJECT_NAME }}/florale-emotion-bot:${TAG}" >> $GITHUB_OUTPUT
|
|
|
|
- name: Login to Harbor Registry
|
|
run: |
|
|
echo "Harbor12345" | docker login ${{ env.HARBOR_REGISTRY }} -u admin --password-stdin
|
|
|
|
- name: Build and Push Frontend (Production + Latest)
|
|
working-directory: ./website
|
|
run: |
|
|
IMAGE_NAME=${{ steps.meta.outputs.frontend_image }}
|
|
LATEST_IMAGE="${{ env.HARBOR_REGISTRY }}/${{ env.PROJECT_NAME }}/florale-emotion-frontend:latest"
|
|
|
|
for attempt in 1 2 3; do
|
|
echo "Build attempt $attempt for production frontend..."
|
|
docker build --no-cache --progress=plain -t "${IMAGE_NAME}" . || {
|
|
echo "Build failed on attempt $attempt"
|
|
if [ $attempt -eq 3 ]; then
|
|
echo "All build attempts failed"
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
continue
|
|
}
|
|
|
|
# Push versioned image
|
|
for push_attempt in 1 2 3; do
|
|
echo "Push attempt $push_attempt for production frontend..."
|
|
docker push "${IMAGE_NAME}" && {
|
|
echo "Production frontend push successful on attempt $push_attempt"
|
|
break
|
|
} || {
|
|
echo "Push failed on attempt $push_attempt"
|
|
if [ $push_attempt -eq 3 ]; then
|
|
echo "All push attempts failed"
|
|
exit 1
|
|
fi
|
|
sleep 30
|
|
}
|
|
done
|
|
|
|
# Create and push latest tag
|
|
echo "Creating latest tag for production..."
|
|
docker tag "${IMAGE_NAME}" "${LATEST_IMAGE}"
|
|
for push_attempt in 1 2 3; do
|
|
echo "Push attempt $push_attempt for latest tag..."
|
|
docker push "${LATEST_IMAGE}" && {
|
|
echo "Latest tag push successful on attempt $push_attempt"
|
|
break
|
|
} || {
|
|
echo "Latest push failed on attempt $push_attempt"
|
|
if [ $push_attempt -eq 3 ]; then
|
|
echo "Latest push attempts failed"
|
|
exit 1
|
|
fi
|
|
sleep 30
|
|
}
|
|
done
|
|
break
|
|
done
|
|
|
|
echo "✅ Production frontend build and push completed successfully!"
|
|
echo "🏷️ Tag: ${TAG}"
|
|
echo "📦 Image: ${IMAGE_NAME}"
|
|
echo "📦 Latest: ${LATEST_IMAGE}"
|
|
|
|
- name: Build and Push Backend (Production + Latest)
|
|
working-directory: ./website/backend
|
|
env:
|
|
IMAGE_NAME: ${{ steps.meta.outputs.backend_image }}
|
|
TAG: ${{ steps.meta.outputs.tag }}
|
|
run: |
|
|
echo "Building backend image: ${IMAGE_NAME}"
|
|
docker build --no-cache --progress=plain -t "${IMAGE_NAME}" .
|
|
echo "Pushing backend image: ${IMAGE_NAME}"
|
|
docker push "${IMAGE_NAME}"
|
|
LATEST_IMAGE="${IMAGE_NAME%:*}:latest"
|
|
echo "Tagging backend as latest: ${LATEST_IMAGE}"
|
|
docker tag "${IMAGE_NAME}" "${LATEST_IMAGE}"
|
|
docker push "${LATEST_IMAGE}"
|
|
echo "✅ Production backend build and push completed successfully!"
|
|
echo "🏷️ Tag: ${TAG}"
|
|
echo "📦 Image: ${IMAGE_NAME}"
|
|
echo "📦 Latest: ${LATEST_IMAGE}"
|
|
|
|
- name: Build and Push Social Media Bot (Production + Latest)
|
|
working-directory: ./website/social-media-bot
|
|
env:
|
|
IMAGE_NAME: ${{ steps.meta.outputs.bot_image }}
|
|
TAG: ${{ steps.meta.outputs.tag }}
|
|
run: |
|
|
echo "Building social media bot image: ${IMAGE_NAME}"
|
|
docker build --no-cache --progress=plain -t "${IMAGE_NAME}" .
|
|
echo "Pushing social media bot image: ${IMAGE_NAME}"
|
|
docker push "${IMAGE_NAME}"
|
|
LATEST_IMAGE="${IMAGE_NAME%:*}:latest"
|
|
echo "Tagging social media bot as latest: ${LATEST_IMAGE}"
|
|
docker tag "${IMAGE_NAME}" "${LATEST_IMAGE}"
|
|
docker push "${LATEST_IMAGE}"
|
|
echo "✅ Production social media bot build and push completed successfully!"
|
|
echo "🏷️ Tag: ${TAG}"
|
|
echo "📦 Image: ${IMAGE_NAME}"
|
|
echo "📦 Latest: ${LATEST_IMAGE}"
|
|
|
|
- name: Setup kubectl
|
|
run: |
|
|
# Install kubectl directly to avoid permission issues
|
|
echo "Installing kubectl..."
|
|
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
|
chmod +x kubectl
|
|
sudo mv kubectl /usr/local/bin/
|
|
kubectl version --client
|
|
|
|
- name: Configure kubectl
|
|
env:
|
|
KUBECTLSECRET: ${{ secrets.KUBECTLSECRET }}
|
|
run: |
|
|
mkdir -p ~/.kube
|
|
|
|
echo "🔍 Debugging KUBECTLSECRET..."
|
|
echo "Secret length: ${#KUBECTLSECRET}"
|
|
|
|
# Check if secret is empty
|
|
if [ "${#KUBECTLSECRET}" -eq 0 ]; then
|
|
echo "❌ ERROR: KUBECTLSECRET is empty!"
|
|
exit 1
|
|
fi
|
|
|
|
# Try to decode as base64 first, if that fails, use as plain text
|
|
if echo "$KUBECTLSECRET" | base64 -d > ~/.kube/config 2>/dev/null; then
|
|
echo "✅ KUBECTLSECRET decoded as base64"
|
|
else
|
|
echo "⚠️ KUBECTLSECRET is not base64, using as plain text"
|
|
echo "$KUBECTLSECRET" > ~/.kube/config
|
|
fi
|
|
|
|
echo "📁 kubeconfig created at ~/.kube/config"
|
|
chmod 600 ~/.kube/config
|
|
|
|
# Debug kubeconfig content (without sensitive data)
|
|
echo "🔍 Debugging kubeconfig structure..."
|
|
echo "File size: $(wc -c < ~/.kube/config) bytes"
|
|
|
|
echo "First few lines of kubeconfig (structure check):"
|
|
head -20 ~/.kube/config | grep -E "(apiVersion|kind|clusters|contexts|users|current-context)" || echo "No standard kubeconfig structure found"
|
|
|
|
echo "Checking for current-context:"
|
|
grep "current-context:" ~/.kube/config || echo "❌ No current-context found"
|
|
|
|
echo "Checking for clusters:"
|
|
grep -A 2 "clusters:" ~/.kube/config || echo "❌ No clusters found"
|
|
|
|
echo "Checking for users:"
|
|
grep -A 2 "users:" ~/.kube/config || echo "❌ No users found"
|
|
|
|
# Fix TLS issues by adding insecure-skip-tls-verify to all clusters
|
|
echo "🔧 Fixing TLS verification for self-signed certificates..."
|
|
|
|
# Get all cluster names and add insecure-skip-tls-verify
|
|
kubectl config get-clusters | tail -n +2 | while read cluster; do
|
|
if [ -n "$cluster" ]; then
|
|
echo "Setting insecure-skip-tls-verify for cluster: $cluster"
|
|
kubectl config set-cluster "$cluster" --insecure-skip-tls-verify=true
|
|
fi
|
|
done
|
|
|
|
echo "✅ TLS configuration completed"
|
|
|
|
- name: Test kubectl connection
|
|
run: |
|
|
kubectl version --client
|
|
kubectl get nodes
|
|
|
|
- name: Deploy to Production
|
|
env:
|
|
FRONTEND_IMAGE: ${{ steps.meta.outputs.frontend_image }}
|
|
BACKEND_IMAGE: ${{ steps.meta.outputs.backend_image }}
|
|
BOT_IMAGE: ${{ steps.meta.outputs.bot_image }}
|
|
run: |
|
|
echo "🚀 Deploying to production..."
|
|
echo "Frontend image: $FRONTEND_IMAGE"
|
|
echo "Backend image: $BACKEND_IMAGE"
|
|
echo "Bot image: $BOT_IMAGE"
|
|
|
|
# Ensure namespace exists
|
|
kubectl create namespace florale-emotion --dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# Ensure Harbor registry secret exists
|
|
kubectl create secret docker-registry harbor-registry-secret \
|
|
--docker-server=${{ env.HARBOR_REGISTRY }} \
|
|
--docker-username=admin \
|
|
--docker-password=Harbor12345 \
|
|
--namespace=florale-emotion \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# Apply frontend deployment und ersetze das Image-Tag mit latest
|
|
sed "s|__IMAGE_TAG__|latest|g" website/k8s/frontend.yaml | kubectl apply -n florale-emotion -f -
|
|
|
|
# Apply backend deployment mit latest Tag
|
|
sed "s|__IMAGE_TAG__|latest|g" website/k8s/backend.yaml | kubectl apply -n florale-emotion -f -
|
|
|
|
# Apply social media bot deployment mit latest Tag
|
|
sed "s|__IMAGE_TAG__|latest|g" website/k8s/bot.yaml | kubectl apply -n florale-emotion -f -
|
|
|
|
# Apply production ingress
|
|
kubectl apply -f website/k8s/ingress.yaml -n florale-emotion
|
|
|
|
# Wait for rollout
|
|
echo "⏳ Waiting for rollout to finish..."
|
|
kubectl rollout status deployment/florale-emotion-frontend -n florale-emotion --timeout=300s
|
|
|
|
echo "✅ Production deployment complete!"
|
|
echo "🌐 Website: https://florale-emotion.de"
|
|
echo "🌐 WWW: https://www.florale-emotion.de"
|
|
echo "🔧 Backend API: https://api.florale-emotion.de"
|
|
|
|
# Hinweise:
|
|
# - Angepasst für florale-emotion Projekt
|
|
# - Namespace: florale-emotion
|
|
# - Dev-Deployment unter dev.florale-emotion.de
|
|
# - Production unter florale-emotion.de
|
|
# - Unterstützt Frontend, Backend und Social Media Bot
|
|
# - Robuste Retry-Mechanismen für Build und Push
|
|
# - Eindeutige Tags für bessere Nachverfolgung
|
|
# - Timeout-Schutz für alle Deployments |