name: 🚀 Build & Deploy Image run-name: Build Image - ${{ github.ref_name }} - ${{ github.sha }} on: push: branches: [ main, staging ] paths-ignore: - '**.md' - 'docs/**' workflow_dispatch: inputs: branch: description: 'Branch to build from' required: false type: choice options: - staging - main - develop default: 'staging' force_build: description: 'Force image build even if no runtime changes detected' type: boolean required: false default: false env: REGISTRY: registry.michaelschiemer.de IMAGE_NAME: framework RUNTIME_IMAGE_NAME: framework-runtime jobs: # Job 0: Detect if a new image build is required changes: name: Determine Build Necessity runs-on: ubuntu-latest outputs: needs_build: ${{ steps.filter.outputs.needs_build }} changed_files: ${{ steps.filter.outputs.changed_files }} needs_runtime_build: ${{ steps.filter.outputs.needs_runtime_build }} steps: - name: Download CI helpers shell: bash env: CI_TOKEN: ${{ secrets.CI_TOKEN }} run: | set -euo pipefail REF="${{ github.sha }}" if [ -z "$REF" ]; then REF="${{ github.ref_name }}" fi if [ -z "$REF" ]; then REF="${{ inputs.branch || 'staging' }}" fi URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh" mkdir -p /tmp/ci-tools if [ -n "$CI_TOKEN" ]; then curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh else curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh fi chmod +x /tmp/ci-tools/clone_repo.sh - name: Analyse changed files id: filter shell: bash env: FORCE_BUILD: ${{ inputs.force_build }} EVENT_NAME: ${{ github.event_name }} EVENT_BEFORE: ${{ github.event.before }} INPUT_BRANCH: ${{ inputs.branch }} REF_NAME_GITHUB: ${{ github.ref_name }} run: | set -euo pipefail FORCE="${FORCE_BUILD:-false}" if [ "$EVENT_NAME" != "workflow_dispatch" ]; then FORCE="false" fi REF_NAME="$REF_NAME_GITHUB" if [ -z "$REF_NAME" ]; then REF_NAME="$INPUT_BRANCH" fi if [ -z "$REF_NAME" ]; then REF_NAME="staging" fi REPO="${{ github.repository }}" WORKDIR="/workspace/repo" export CI_REPOSITORY="$REPO" export CI_TOKEN="${{ secrets.CI_TOKEN }}" export CI_REF_NAME="$REF_NAME" export CI_INPUT_BRANCH="$INPUT_BRANCH" export CI_DEFAULT_BRANCH="main" export CI_TARGET_DIR="$WORKDIR" export CI_FETCH_DEPTH="2" /tmp/ci-tools/clone_repo.sh cd "$WORKDIR" CHANGED_FILES="" if [ "$EVENT_NAME" = "push" ] && [ -n "$EVENT_BEFORE" ]; then if git rev-parse "$EVENT_BEFORE" >/dev/null 2>&1; then CHANGED_FILES="$(git diff --name-only "$EVENT_BEFORE" HEAD || true)" else git fetch origin "$EVENT_BEFORE" --depth 1 || true if git rev-parse "$EVENT_BEFORE" >/dev/null 2>&1; then CHANGED_FILES="$(git diff --name-only "$EVENT_BEFORE" HEAD || true)" fi fi fi if [ -z "$CHANGED_FILES" ]; then if git rev-parse HEAD^ >/dev/null 2>&1; then CHANGED_FILES="$(git diff --name-only HEAD^ HEAD || true)" else echo "ℹ️ Erweiterter Fetch für Diff-Ermittlung" git fetch origin "$REF_NAME" --depth 50 || true if git rev-parse HEAD^ >/dev/null 2>&1; then CHANGED_FILES="$(git diff --name-only HEAD^ HEAD || true)" fi fi fi if [ -z "$CHANGED_FILES" ] && [ "$FORCE" != "true" ]; then # No diff information available; assume no build needed if this is not initial commit # Only skip if we can detect this is not the first commit if git rev-parse HEAD^ >/dev/null 2>&1; then echo "ℹ️ Keine Änderungsinformation gefunden, aber HEAD^ existiert – überspringe Build." echo "needs_build=false" >> "$GITHUB_OUTPUT" echo "changed_files=" >> "$GITHUB_OUTPUT" echo "needs_runtime_build=false" >> "$GITHUB_OUTPUT" exit 0 else # First commit or detached state - build to be safe echo "⚠️ Keine Änderungsinformation gefunden – bilde Image sicherheitshalber." echo "needs_build=true" >> "$GITHUB_OUTPUT" echo "changed_files=" >> "$GITHUB_OUTPUT" echo "needs_runtime_build=true" >> "$GITHUB_OUTPUT" exit 0 fi fi NEEDS_BUILD=true SUMMARY="Runtime-relevante Änderungen erkannt" RUNTIME_BUILD=false RUNTIME_PATTERN='^(Dockerfile\\.production|Dockerfile\\.runtime|docker/php/|install-php85\\.sh)' if [ -n "$CHANGED_FILES" ]; then NEEDS_BUILD=false OTHER_NON_IGNORED=false IGNORE_PATTERN='^(docs/|docs$|tests/|tests$|tests-e2e/|\.github/|\.gitea/|\.idea/|\.vscode/|\.husky/|.*\.md$|.*\.MD$|LICENSE$|CHANGELOG|CHANGELOG\.md$|\.editorconfig$|\.gitignore$)' BUILD_TRIGGER_PATTERN='^(src/|resources/|config/|app/|public/|composer\.json$|composer\.lock$|composer/|package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|Dockerfile\.production$|Dockerfile\.runtime$|docker-compose\..*\.yml$|docker-compose\.yml$|docker/|vite\.config\.(js|ts)$|tsconfig\.json$|babel\.config\.js$|Makefile$|artisan$)' while IFS= read -r FILE; do [ -z "$FILE" ] && continue if echo "$FILE" | grep -Eq "$RUNTIME_PATTERN"; then RUNTIME_BUILD=true fi if echo "$FILE" | grep -Eq "$IGNORE_PATTERN"; then continue fi if echo "$FILE" | grep -Eq "$BUILD_TRIGGER_PATTERN"; then NEEDS_BUILD=true continue fi OTHER_NON_IGNORED=true done <<< "$CHANGED_FILES" if [ "$NEEDS_BUILD" = "false" ] && [ "$OTHER_NON_IGNORED" = "false" ]; then SUMMARY="Nur Doku-/Teständerungen – Container-Build wird übersprungen" elif [ "$NEEDS_BUILD" = "false" ] && [ "$OTHER_NON_IGNORED" = "true" ]; then SUMMARY="Keine Build-Trigger gefunden – Container-Build wird übersprungen" elif [ "$NEEDS_BUILD" = "true" ]; then SUMMARY="Runtime-relevante Änderungen erkannt – Container-Build wird ausgeführt" fi else RUNTIME_BUILD=true fi if [ "$FORCE" = "true" ]; then NEEDS_BUILD=true SUMMARY="Manuell erzwungener Build" RUNTIME_BUILD=true fi PRETTY_CHANGES="$(printf '%s' "$CHANGED_FILES" | tr '\n' ', ' | sed 's/, $//')" if [ -z "$PRETTY_CHANGES" ]; then PRETTY_CHANGES="" fi echo "📄 Änderungen: $PRETTY_CHANGES" echo "ℹ️ Ergebnis: $SUMMARY" echo "🔁 Runtime-Rebuild erforderlich: $RUNTIME_BUILD" echo "needs_build=$NEEDS_BUILD" >> "$GITHUB_OUTPUT" echo "changed_files=$PRETTY_CHANGES" >> "$GITHUB_OUTPUT" echo "needs_runtime_build=$RUNTIME_BUILD" >> "$GITHUB_OUTPUT" runtime-base: name: Build Runtime Base Image needs: changes if: needs.changes.outputs.needs_runtime_build == 'true' runs-on: docker-build outputs: image_ref: ${{ steps.set-result.outputs.image_ref }} built: ${{ steps.set-result.outputs.built }} steps: - name: Download CI helpers shell: bash env: CI_TOKEN: ${{ secrets.CI_TOKEN }} run: | set -euo pipefail REF="${{ github.sha }}" if [ -z "$REF" ]; then REF="${{ github.ref_name }}" fi if [ -z "$REF" ]; then REF="${{ inputs.branch || 'staging' }}" fi URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh" mkdir -p /tmp/ci-tools if [ -n "$CI_TOKEN" ]; then curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh else curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh fi chmod +x /tmp/ci-tools/clone_repo.sh - name: Evaluate runtime build requirement id: decision shell: bash run: | if [ "${{ needs.changes.outputs.needs_runtime_build }}" = "true" ]; then echo "Runtime base rebuild required" echo "should_build=true" >> "$GITHUB_OUTPUT" else echo "Runtime base rebuild not required" echo "should_build=false" >> "$GITHUB_OUTPUT" fi - name: Install git and setup environment if: steps.decision.outputs.should_build == 'true' shell: sh run: | if ! command -v bash >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1; then apk add --no-cache git bash curl fi bash --version git --version - name: Checkout code if: steps.decision.outputs.should_build == 'true' shell: bash run: | REF_NAME="${{ github.ref_name }}" INPUT_BRANCH="${{ inputs.branch }}" REPO="${{ github.repository }}" export CI_REPOSITORY="$REPO" export CI_TOKEN="${{ secrets.CI_TOKEN }}" export CI_REF_NAME="$REF_NAME" export CI_INPUT_BRANCH="$INPUT_BRANCH" export CI_DEFAULT_BRANCH="main" export CI_TARGET_DIR="/workspace/repo" export CI_FETCH_DEPTH="1" /tmp/ci-tools/clone_repo.sh cd /workspace/repo - name: Setup Docker Buildx if: steps.decision.outputs.should_build == 'true' shell: bash run: | docker buildx version || echo "Buildx nicht gefunden" if ! docker ps >/dev/null 2>&1; then echo "❌ Fehler: Docker ist nicht verfügbar!" exit 1 fi if ! docker buildx ls 2>/dev/null | grep -q builder; then docker buildx create --name builder --use --driver docker-container else docker buildx use builder fi docker buildx inspect --bootstrap - name: Login to Registry if: steps.decision.outputs.should_build == 'true' shell: bash run: | REGISTRY_USER="${{ secrets.REGISTRY_USER }}" REGISTRY_PASSWORD="${{ secrets.REGISTRY_PASSWORD }}" REGISTRY_URL="${{ env.REGISTRY }}" DEPLOYMENT_HOST="94.16.110.151" if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_PASSWORD" ]; then echo "❌ Error: Registry credentials missing" exit 1 fi echo "🔐 Logging in to registry..." HOST_IP=$(ip route | grep default | awk '{print $3}' 2>/dev/null | head -1 || echo "$DEPLOYMENT_HOST") REGISTRY_URLS=( "registry.michaelschiemer.de" "$REGISTRY_URL" "$DEPLOYMENT_HOST" "$DEPLOYMENT_HOST:5000" "${HOST_IP}:5000" ) LOGIN_SUCCESS=false for TEST_URL in "${REGISTRY_URLS[@]}"; do echo "🔍 Testing registry: $TEST_URL" if [[ "$TEST_URL" == *":5000" ]]; then HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://$TEST_URL/v2/" 2>&1 || echo "000") if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "200" ]; then set +e LOGIN_OUTPUT=$(echo "$REGISTRY_PASSWORD" | docker login "$TEST_URL" -u "$REGISTRY_USER" --password-stdin 2>&1) LOGIN_EXIT_CODE=$? set -e if [ $LOGIN_EXIT_CODE -eq 0 ]; then REGISTRY_URL="$TEST_URL" LOGIN_SUCCESS=true break fi fi else HTTPS_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "https://$TEST_URL/v2/" 2>&1 || echo "000") if [ "$HTTPS_CODE" = "401" ] || [ "$HTTPS_CODE" = "200" ]; then set +e LOGIN_OUTPUT=$(echo "$REGISTRY_PASSWORD" | docker login "$TEST_URL" -u "$REGISTRY_USER" --password-stdin 2>&1) LOGIN_EXIT_CODE=$? set -e if [ $LOGIN_EXIT_CODE -eq 0 ]; then REGISTRY_URL="$TEST_URL" LOGIN_SUCCESS=true break fi fi fi done if [ "$LOGIN_SUCCESS" = false ]; then echo "❌ Registry login failed" exit 1 fi echo "✅ Registry login successful: $REGISTRY_URL" echo "REGISTRY_URL=$REGISTRY_URL" >> $GITHUB_ENV echo "CACHE_REGISTRY=${{ env.REGISTRY }}" >> $GITHUB_ENV - name: Build and push runtime base image if: steps.decision.outputs.should_build == 'true' shell: bash env: REGISTRY_URL: ${{ env.REGISTRY_URL }} CACHE_REGISTRY: ${{ env.CACHE_REGISTRY }} RUNTIME_IMAGE_NAME: ${{ env.RUNTIME_IMAGE_NAME }} run: | cd /workspace/repo TARGET_REGISTRY="$CACHE_REGISTRY" if [ -z "$TARGET_REGISTRY" ]; then TARGET_REGISTRY="$REGISTRY_URL" fi echo "TARGET_REGISTRY=$TARGET_REGISTRY" >> $GITHUB_ENV # Debug: Check if RUNTIME_IMAGE_NAME is set if [ -z "$RUNTIME_IMAGE_NAME" ]; then echo "⚠️ RUNTIME_IMAGE_NAME is not set, using default: framework-runtime" RUNTIME_IMAGE_NAME="framework-runtime" fi IMAGE_NAME="$RUNTIME_IMAGE_NAME" echo "🏗️ Building runtime base image..." echo " Registry: $TARGET_REGISTRY" echo " Image: $IMAGE_NAME" docker buildx build \ --platform linux/amd64 \ --file ./Dockerfile.production \ --target runtime-base \ --tag "$TARGET_REGISTRY/$IMAGE_NAME:latest" \ --tag "$TARGET_REGISTRY/$IMAGE_NAME:$(date +%Y%m%d)" \ --push \ . echo "✅ Runtime base image pushed successfully!" - name: Set runtime base outputs id: set-result shell: bash env: RUNTIME_IMAGE_NAME: ${{ env.RUNTIME_IMAGE_NAME }} run: | if [ "${{ steps.decision.outputs.should_build }}" = "true" ]; then TARGET_REGISTRY="${{ env.TARGET_REGISTRY || env.REGISTRY }}" if [ -z "$TARGET_REGISTRY" ]; then TARGET_REGISTRY="${{ env.REGISTRY }}" fi echo "image_ref=$TARGET_REGISTRY/$RUNTIME_IMAGE_NAME:latest" >> "$GITHUB_OUTPUT" echo "built=true" >> "$GITHUB_OUTPUT" else # When runtime build is skipped, output empty but build job will use default latest image echo "image_ref=" >> "$GITHUB_OUTPUT" echo "built=false" >> "$GITHUB_OUTPUT" fi # Job 1: Run Tests test: needs: changes if: needs.changes.outputs.needs_build == 'true' name: Run Tests & Quality Checks runs-on: php-ci steps: - name: Download CI helpers shell: bash env: CI_TOKEN: ${{ secrets.CI_TOKEN }} run: | set -euo pipefail REF="${{ github.sha }}" if [ -z "$REF" ]; then REF="${{ github.ref_name }}" fi if [ -z "$REF" ]; then REF="${{ inputs.branch || 'staging' }}" fi URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh" mkdir -p /tmp/ci-tools if [ -n "$CI_TOKEN" ]; then curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh else curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh fi chmod +x /tmp/ci-tools/clone_repo.sh - name: Checkout code run: | REF_NAME="${{ github.ref_name }}" INPUT_BRANCH="${{ inputs.branch }}" REPO="${{ github.repository }}" export CI_REPOSITORY="$REPO" export CI_TOKEN="${{ secrets.CI_TOKEN }}" export CI_REF_NAME="$REF_NAME" export CI_INPUT_BRANCH="$INPUT_BRANCH" export CI_DEFAULT_BRANCH="main" export CI_TARGET_DIR="/workspace/repo" export CI_FETCH_DEPTH="1" /tmp/ci-tools/clone_repo.sh cd /workspace/repo - name: Cache Composer dependencies run: | if [ -d "/tmp/composer-cache/vendor" ]; then echo "📦 Restoring cached dependencies..." cp -r /tmp/composer-cache/vendor /workspace/repo/vendor || true fi - name: Install dependencies run: | cd /workspace/repo composer install --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-req=php - name: Save Composer cache run: | mkdir -p /tmp/composer-cache cp -r /workspace/repo/vendor /tmp/composer-cache/vendor || true - name: Tests temporarily skipped run: | echo "⚠️ Tests temporarily skipped due to PHP 8.5 compatibility issues" # Job 2: Build & Push Docker Image build: name: Build Docker Image needs: [changes, runtime-base] runs-on: docker-build env: SHOULD_BUILD: ${{ needs.changes.outputs.needs_build }} outputs: image_tag: ${{ steps.image_info.outputs.IMAGE_TAG }} commit_sha: ${{ steps.meta.outputs.commit_sha }} image_url: ${{ steps.image_info.outputs.IMAGE_URL }} steps: - name: Skip build when not required if: ${{ env.SHOULD_BUILD != 'true' }} run: | echo "ℹ️ Container build not required – using latest published image" - name: Install git and setup environment if: ${{ env.SHOULD_BUILD == 'true' }} shell: sh run: | if ! command -v bash >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1; then apk add --no-cache git bash curl fi bash --version git --version - name: Download CI helpers if: ${{ env.SHOULD_BUILD == 'true' }} shell: bash env: CI_TOKEN: ${{ secrets.CI_TOKEN }} run: | set -euo pipefail REF="${{ github.sha }}" if [ -z "$REF" ]; then REF="${{ github.ref_name }}" fi if [ -z "$REF" ]; then REF="${{ inputs.branch || 'staging' }}" fi URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh" mkdir -p /tmp/ci-tools if [ -n "$CI_TOKEN" ]; then curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh else curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh fi chmod +x /tmp/ci-tools/clone_repo.sh - name: Checkout code if: ${{ env.SHOULD_BUILD == 'true' }} shell: bash run: | REF_NAME="${{ github.ref_name }}" INPUT_BRANCH="${{ inputs.branch }}" REPO="${{ github.repository }}" export CI_REPOSITORY="$REPO" export CI_TOKEN="${{ secrets.CI_TOKEN }}" export CI_REF_NAME="$REF_NAME" export CI_INPUT_BRANCH="$INPUT_BRANCH" export CI_DEFAULT_BRANCH="main" export CI_TARGET_DIR="/workspace/repo" export CI_FETCH_DEPTH="1" /tmp/ci-tools/clone_repo.sh cd /workspace/repo - name: Setup Docker Buildx if: ${{ env.SHOULD_BUILD == 'true' }} shell: bash run: | docker buildx version || echo "Buildx nicht gefunden" echo "🔧 DOCKER_HOST: ${DOCKER_HOST:-nicht gesetzt}" docker info | grep -E "Server Version|Registry" || true if ! docker ps >/dev/null 2>&1; then echo "❌ Fehler: Docker ist nicht verfügbar!" exit 1 fi if ! docker buildx ls 2>/dev/null | grep -q builder; then echo "📦 Erstelle neuen Buildx Builder..." docker buildx create --name builder --use --driver docker-container else echo "✅ Builder existiert bereits" docker buildx use builder fi docker buildx inspect --bootstrap docker buildx ls - name: Generate image metadata if: ${{ env.SHOULD_BUILD == 'true' }} id: meta run: | cd /workspace/repo COMMIT_SHA="${{ github.sha }}" if [ -z "$COMMIT_SHA" ]; then COMMIT_SHA=$(git rev-parse HEAD) fi SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) TAG="${SHORT_SHA}-$(date +%s)" echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT echo "Generated tag: ${TAG}" - name: Login to Registry if: ${{ env.SHOULD_BUILD == 'true' }} id: login shell: bash run: | REGISTRY_USER="${{ secrets.REGISTRY_USER }}" REGISTRY_PASSWORD="${{ secrets.REGISTRY_PASSWORD }}" REGISTRY_URL="${{ env.REGISTRY }}" CANONICAL_REGISTRY="${{ env.REGISTRY }}" DEPLOYMENT_HOST="94.16.110.151" if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_PASSWORD" ]; then echo "❌ Error: Registry credentials missing" exit 1 fi echo "🔐 Logging in to registry..." HOST_IP=$(ip route | grep default | awk '{print $3}' 2>/dev/null | head -1 || echo "$DEPLOYMENT_HOST") REGISTRY_URLS=( "registry.michaelschiemer.de" "$REGISTRY_URL" "$DEPLOYMENT_HOST" "$DEPLOYMENT_HOST:5000" "${HOST_IP}:5000" ) LOGIN_SUCCESS=false for TEST_URL in "${REGISTRY_URLS[@]}"; do echo "🔍 Testing registry: $TEST_URL" if [[ "$TEST_URL" == *":5000" ]]; then HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://$TEST_URL/v2/" 2>&1 || echo "000") if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "200" ]; then set +e LOGIN_OUTPUT=$(echo "$REGISTRY_PASSWORD" | docker login "$TEST_URL" -u "$REGISTRY_USER" --password-stdin 2>&1) LOGIN_EXIT_CODE=$? set -e if [ $LOGIN_EXIT_CODE -eq 0 ]; then REGISTRY_URL="$TEST_URL" LOGIN_SUCCESS=true break fi fi else HTTPS_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "https://$TEST_URL/v2/" 2>&1 || echo "000") if [ "$HTTPS_CODE" = "401" ] || [ "$HTTPS_CODE" = "200" ]; then set +e LOGIN_OUTPUT=$(echo "$REGISTRY_PASSWORD" | docker login "$TEST_URL" -u "$REGISTRY_USER" --password-stdin 2>&1) LOGIN_EXIT_CODE=$? set -e if [ $LOGIN_EXIT_CODE -eq 0 ]; then REGISTRY_URL="$TEST_URL" LOGIN_SUCCESS=true break fi fi fi done if [ "$LOGIN_SUCCESS" = false ]; then echo "❌ Registry login failed" exit 1 fi echo "✅ Registry login successful: $REGISTRY_URL" echo "REGISTRY_URL=$REGISTRY_URL" >> $GITHUB_ENV echo "CACHE_REGISTRY=$CANONICAL_REGISTRY" >> $GITHUB_ENV - name: Determine runtime base image if: ${{ env.SHOULD_BUILD == 'true' }} id: runtime-image shell: bash run: | DEFAULT_IMAGE="${{ env.REGISTRY }}/${{ env.RUNTIME_IMAGE_NAME }}:latest" SELECTED_IMAGE="$DEFAULT_IMAGE" # Try to get runtime-base image_ref (may not be available if runtime-base was skipped) RUNTIME_BASE_REF="${{ needs.runtime-base.outputs.image_ref }}" if [ -n "$RUNTIME_BASE_REF" ] && [ "$RUNTIME_BASE_REF" != "" ] && [ "$RUNTIME_BASE_REF" != "null" ]; then SELECTED_IMAGE="$RUNTIME_BASE_REF" echo "ℹ️ Verwende frisch gebautes Runtime-Image: $SELECTED_IMAGE" else if ! docker pull "$DEFAULT_IMAGE" >/dev/null 2>&1; then ALT_REGISTRY="${{ env.REGISTRY_URL }}" if [ -n "$ALT_REGISTRY" ] && [ "$ALT_REGISTRY" != "$DEFAULT_IMAGE" ]; then ALT_IMAGE="$ALT_REGISTRY/${{ env.RUNTIME_IMAGE_NAME }}:latest" if docker pull "$ALT_IMAGE" >/dev/null 2>&1; then echo "ℹ️ Verwende alternatives Runtime-Image $ALT_IMAGE" SELECTED_IMAGE="$ALT_IMAGE" else echo "⚠️ Runtime base image nicht verfügbar, verwende lokale Stage" SELECTED_IMAGE="runtime-base" fi else echo "⚠️ Runtime base image nicht verfügbar, verwende lokale Stage" SELECTED_IMAGE="runtime-base" fi else echo "ℹ️ Verwende bestehendes Runtime-Image $DEFAULT_IMAGE" fi fi echo "runtime_image=$SELECTED_IMAGE" >> "$GITHUB_OUTPUT" echo "🏗️ Runtime-Image gewählt: $SELECTED_IMAGE" - name: Build and push Docker image if: ${{ env.SHOULD_BUILD == 'true' }} shell: bash env: REGISTRY_URL: ${{ env.REGISTRY_URL }} CACHE_REGISTRY: ${{ env.CACHE_REGISTRY }} RUNTIME_IMAGE: ${{ steps.runtime-image.outputs.runtime_image }} run: | cd /workspace/repo REGISTRY_TO_USE="$REGISTRY_URL" CACHE_TARGET="$CACHE_REGISTRY" if [ -z "$CACHE_TARGET" ]; then CACHE_TARGET="$REGISTRY_TO_USE" fi IMAGE_NAME="${{ env.IMAGE_NAME }}" COMMIT_SHA="${{ github.sha }}" if [ -z "$COMMIT_SHA" ]; then COMMIT_SHA=$(git rev-parse HEAD) fi REF_NAME="${{ github.ref_name }}" if [ -z "$REF_NAME" ]; then REF_NAME=$(git rev-parse --abbrev-ref HEAD) fi SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) TAG="${SHORT_SHA}-$(date +%s)" echo "🏗️ Building Docker image..." echo " Registry: $REGISTRY_TO_USE" if [ "$CACHE_TARGET" != "$REGISTRY_TO_USE" ]; then echo " Cache registry: $CACHE_TARGET" fi echo " Runtime base: $RUNTIME_IMAGE" echo " Image: $IMAGE_NAME" echo " Tags: latest, $TAG, git-$SHORT_SHA" # Build cache sources - branch-specific and general caches CACHE_SOURCES=( "type=registry,ref=${CACHE_TARGET}/${IMAGE_NAME}:buildcache" "type=registry,ref=${REGISTRY_TO_USE}/${IMAGE_NAME}:latest" "type=registry,ref=${REGISTRY_TO_USE}/${IMAGE_NAME}:${REF_NAME}-cache" ) # If this is not the first build, try to use previous commit's tag as cache if git rev-parse HEAD^ >/dev/null 2>&1; then PREV_SHORT_SHA=$(git rev-parse --short=7 HEAD^) CACHE_SOURCES+=("type=registry,ref=${REGISTRY_TO_USE}/${IMAGE_NAME}:git-${PREV_SHORT_SHA}") fi CACHE_FROM_ARGS="" for CACHE_SRC in "${CACHE_SOURCES[@]}"; do CACHE_FROM_ARGS="${CACHE_FROM_ARGS} --cache-from ${CACHE_SRC}" done docker buildx build \ --platform linux/amd64 \ --file ./Dockerfile.production \ --build-arg RUNTIME_IMAGE="$RUNTIME_IMAGE" \ --tag "${REGISTRY_TO_USE}/${IMAGE_NAME}:latest" \ --tag "${REGISTRY_TO_USE}/${IMAGE_NAME}:${TAG}" \ --tag "${REGISTRY_TO_USE}/${IMAGE_NAME}:git-${SHORT_SHA}" \ ${CACHE_FROM_ARGS} \ --cache-to type=registry,ref="${CACHE_TARGET}/${IMAGE_NAME}:buildcache",mode=max \ --cache-to type=registry,ref="${REGISTRY_TO_USE}/${IMAGE_NAME}:${REF_NAME}-cache",mode=max \ --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ --build-arg GIT_COMMIT=${COMMIT_SHA} \ --build-arg GIT_BRANCH=${REF_NAME} \ --push \ . echo "✅ Image built and pushed successfully!" - name: Set image info if: ${{ env.SHOULD_BUILD == 'true' }} id: image_info shell: bash run: | COMMIT_SHA="${{ github.sha }}" if [ -z "$COMMIT_SHA" ]; then COMMIT_SHA=$(cd /workspace/repo && git rev-parse HEAD) fi SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) TAG="${SHORT_SHA}-$(date +%s)" REGISTRY_TO_USE="${{ env.REGISTRY_URL }}" IMAGE_NAME="${{ env.IMAGE_NAME }}" IMAGE_TAG="$TAG" IMAGE_URL="${REGISTRY_TO_USE}/${IMAGE_NAME}:${IMAGE_TAG}" echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "IMAGE_URL=$IMAGE_URL" >> $GITHUB_OUTPUT echo "📦 Image info:" echo " Tag: $IMAGE_TAG" echo " URL: $IMAGE_URL" echo "" REF_NAME="${{ github.ref_name }}" if [ -z "$REF_NAME" ]; then REF_NAME=$(cd /workspace/repo && git rev-parse --abbrev-ref HEAD) fi if [ "$REF_NAME" = "staging" ]; then echo "🚀 Staging branch detected - will auto-deploy after build" else echo "💡 Image is ready for deployment!" echo " Run the 'Deploy to Production' or 'Deploy to Staging' workflow to deploy this image." fi # Job 3: Auto-deploy to Staging (only for staging branch) deploy-staging: name: Auto-deploy to Staging needs: [changes] if: github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging')) runs-on: ubuntu-latest environment: name: staging url: https://staging.michaelschiemer.de env: DEPLOYMENT_HOST: 94.16.110.151 steps: - name: Determine branch name id: branch shell: bash run: | REF_NAME="${{ github.ref_name }}" if [ -z "$REF_NAME" ]; then REF_NAME=$(echo "${{ github.ref }}" | sed 's/refs\/heads\///') fi if [ -z "$REF_NAME" ]; then REF_NAME="staging" fi echo "BRANCH=$REF_NAME" >> $GITHUB_OUTPUT echo "📋 Branch: $REF_NAME" - name: Checkout deployment scripts run: | REF_NAME="${{ steps.branch.outputs.BRANCH }}" REPO="${{ github.repository }}" if [ -n "${{ secrets.CI_TOKEN }}" ]; then git clone --depth 1 --branch "$REF_NAME" \ "https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \ /workspace/repo else git clone --depth 1 --branch "$REF_NAME" \ "https://git.michaelschiemer.de/${REPO}.git" \ /workspace/repo || \ git clone --depth 1 \ "https://git.michaelschiemer.de/${REPO}.git" \ /workspace/repo fi cd /workspace/repo - name: Setup SSH key run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production chmod 600 ~/.ssh/production ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts - name: Deploy to Staging Server run: | set -e DEPLOYMENT_HOST="${{ env.DEPLOYMENT_HOST }}" REGISTRY_HOST="${{ env.REGISTRY }}" IMAGE_NAME="${{ env.IMAGE_NAME }}" DEFAULT_IMAGE="${REGISTRY_HOST}/${IMAGE_NAME}:latest" # Always use latest image - if a build happened, it would have pushed to latest anyway # Using latest ensures we always get the most recent image, whether it was just built or not SELECTED_IMAGE="$DEFAULT_IMAGE" STACK_PATH_DISPLAY="~/deployment/stacks/staging" SELECTED_TAG="${SELECTED_IMAGE##*:}" SELECTED_REPO="${SELECTED_IMAGE%:*}" if [ -z "$SELECTED_REPO" ] || [ "$SELECTED_REPO" = "$SELECTED_IMAGE" ]; then FALLBACK_IMAGE="$DEFAULT_IMAGE" else FALLBACK_IMAGE="${SELECTED_REPO}:latest" fi echo "🚀 Starting staging deployment..." echo " Image: ${SELECTED_IMAGE}" echo " Tag: ${SELECTED_TAG}" echo " Host: ${DEPLOYMENT_HOST}" echo " Stack: ${STACK_PATH_DISPLAY}" FULL_IMAGE_ARG=$(printf '%q' "$SELECTED_IMAGE") FALLBACK_IMAGE_ARG=$(printf '%q' "$FALLBACK_IMAGE") IMAGE_NAME_ARG=$(printf '%q' "$IMAGE_NAME") REGISTRY_ARG=$(printf '%q' "$REGISTRY_HOST") ssh -i ~/.ssh/production \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ deploy@${DEPLOYMENT_HOST} "bash -s -- $FULL_IMAGE_ARG $FALLBACK_IMAGE_ARG $IMAGE_NAME_ARG $REGISTRY_ARG" <<'EOF' set -e FULL_IMAGE="$1" FALLBACK_IMAGE="$2" IMAGE_NAME="$3" REGISTRY="$4" shift 4 CURRENT_USER="$(whoami)" USER_HOME="$(getent passwd "$CURRENT_USER" | cut -d: -f6 2>/dev/null)" [ -z "$USER_HOME" ] && USER_HOME="$HOME" [ -z "$USER_HOME" ] && USER_HOME="/home/$CURRENT_USER" STACK_TARGET="${USER_HOME}/deployment/stacks/staging" # Ensure staging stack directory exists mkdir -p "${STACK_TARGET}" cd "${STACK_TARGET}" declare -a REGISTRY_TARGETS=() if [ -n "${REGISTRY}" ]; then REGISTRY_TARGETS+=("${REGISTRY}") fi for IMAGE_REF in "${FULL_IMAGE}" "${FALLBACK_IMAGE}"; do if [ -n "${IMAGE_REF}" ]; then HOST_PART="${IMAGE_REF%%/*}" if [ -n "${HOST_PART}" ]; then if ! printf '%s\n' "${REGISTRY_TARGETS[@]}" | grep -qx "${HOST_PART}"; then REGISTRY_TARGETS+=("${HOST_PART}") fi fi fi done for TARGET in "${REGISTRY_TARGETS[@]}"; do [ -z "${TARGET}" ] && continue echo "🔐 Logging in to Docker registry ${TARGET}..." echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${TARGET}" \ -u "${{ secrets.REGISTRY_USER }}" \ --password-stdin || echo "⚠️ Registry login failed for ${TARGET}, continuing..." done DEPLOY_IMAGE="$FULL_IMAGE" echo "📥 Pulling image ${DEPLOY_IMAGE}..." if ! docker pull "${DEPLOY_IMAGE}"; then if [ -n "${FALLBACK_IMAGE}" ] && [ "${DEPLOY_IMAGE}" != "${FALLBACK_IMAGE}" ]; then echo "⚠️ Failed to pull ${DEPLOY_IMAGE}, attempting fallback ${FALLBACK_IMAGE}" if docker pull "${FALLBACK_IMAGE}"; then DEPLOY_IMAGE="${FALLBACK_IMAGE}" echo "ℹ️ Using fallback image ${DEPLOY_IMAGE}" else echo "❌ Failed to pull fallback image ${FALLBACK_IMAGE}" exit 1 fi else echo "❌ Failed to pull image ${DEPLOY_IMAGE}" exit 1 fi fi # Copy base and staging docker-compose files if they don't exist if [ ! -f docker-compose.base.yml ]; then echo "⚠️ docker-compose.base.yml not found, copying from repo..." cp /workspace/repo/docker-compose.base.yml . || { echo "❌ Failed to copy docker-compose.base.yml" exit 1 } fi if [ ! -f docker-compose.staging.yml ]; then echo "⚠️ docker-compose.staging.yml not found, copying from repo..." cp /workspace/repo/docker-compose.staging.yml . || { echo "❌ Failed to copy docker-compose.staging.yml" exit 1 } fi # Update docker-compose.staging.yml with new image tag echo "📝 Updating docker-compose.staging.yml with new image tag..." sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${DEPLOY_IMAGE}|g" docker-compose.staging.yml echo "✅ Updated docker-compose.staging.yml:" grep "image:" docker-compose.staging.yml | head -5 # Ensure networks exist echo "🔗 Ensuring Docker networks exist..." docker network create traefik-public 2>/dev/null || true docker network create staging-internal 2>/dev/null || true echo "🔄 Starting/updating services..." # Use --pull missing instead of --pull always since we already pulled the specific image docker compose -f docker-compose.base.yml -f docker-compose.staging.yml up -d --pull missing --force-recreate || { echo "❌ Failed to start services" exit 1 } echo "⏳ Waiting for services to start..." sleep 15 # Pull latest code from Git repository - always sync code when deploying echo "🔄 Pulling latest code from Git repository in staging-app container..." docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-app bash -c "cd /var/www/html && git -c safe.directory=/var/www/html fetch origin staging && git -c safe.directory=/var/www/html reset --hard origin/staging && git -c safe.directory=/var/www/html clean -fd" || echo "⚠️ Git pull failed, container will sync on next restart" # Also trigger a restart to ensure entrypoint script runs echo "🔄 Restarting staging-app to ensure all services are up-to-date..." docker compose -f docker-compose.base.yml -f docker-compose.staging.yml restart staging-app || echo "⚠️ Failed to restart staging-app" # Fix nginx upstream configuration - critical fix for 502 errors # sites-available/default uses 127.0.0.1:9000 but PHP-FPM runs in staging-app container echo "🔧 Fixing nginx PHP-FPM upstream configuration (post-deploy fix)..." sleep 5 docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx sed -i '/upstream php-upstream {/,/}/s|server 127.0.0.1:9000;|server staging-app:9000;|g' /etc/nginx/sites-available/default || echo "⚠️ Upstream fix (127.0.0.1) failed" docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx sed -i '/upstream php-upstream {/,/}/s|server localhost:9000;|server staging-app:9000;|g' /etc/nginx/sites-available/default || echo "⚠️ Upstream fix (localhost) failed" docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx nginx -t && docker compose -f docker-compose.base.yml -f docker-compose.staging.yml restart staging-nginx || echo "⚠️ Nginx config test or restart failed" echo "✅ Nginx configuration fixed and reloaded" echo "⏳ Waiting for services to stabilize..." sleep 10 echo "📊 Container status:" docker compose -f docker-compose.base.yml -f docker-compose.staging.yml ps echo "✅ Staging deployment completed!" EOF - name: Wait for deployment to stabilize run: sleep 30 - name: Health check id: health run: | for i in {1..10}; do if curl -f -k https://staging.michaelschiemer.de/health; then echo "✅ Health check passed" exit 0 fi echo "⏳ Waiting for staging service... (attempt $i/10)" sleep 10 done echo "❌ Health check failed" exit 1 - name: Notify deployment success if: success() run: | echo "🚀 Staging deployment successful!" echo "URL: https://staging.michaelschiemer.de" echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" # Job 4: Auto-deploy to Production (only for main branch) deploy-production: name: Auto-deploy to Production needs: [changes, build] if: always() && (github.ref_name == 'main' || github.head_ref == 'main' || (github.ref_name == '' && contains(github.ref, 'main'))) && needs.changes.outputs.needs_build == 'true' runs-on: ubuntu-latest environment: name: production url: https://michaelschiemer.de env: DEPLOYMENT_HOST: 94.16.110.151 steps: - name: Determine branch name id: branch shell: bash run: | REF_NAME="${{ github.ref_name }}" if [ -z "$REF_NAME" ]; then REF_NAME=$(echo "${{ github.ref }}" | sed 's/refs\/heads\///') fi if [ -z "$REF_NAME" ]; then REF_NAME="main" fi echo "BRANCH=$REF_NAME" >> $GITHUB_OUTPUT echo "📋 Branch: $REF_NAME" - name: Checkout deployment scripts run: | REF_NAME="${{ steps.branch.outputs.BRANCH }}" REPO="${{ github.repository }}" if [ -n "${{ secrets.CI_TOKEN }}" ]; then git clone --depth 1 --branch "$REF_NAME" \ "https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \ /workspace/repo else git clone --depth 1 --branch "$REF_NAME" \ "https://git.michaelschiemer.de/${REPO}.git" \ /workspace/repo || \ git clone --depth 1 \ "https://git.michaelschiemer.de/${REPO}.git" \ /workspace/repo fi cd /workspace/repo - name: Setup SSH key run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production chmod 600 ~/.ssh/production ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts - name: Deploy to Production Server run: | set -e DEPLOYMENT_HOST="${{ env.DEPLOYMENT_HOST }}" REGISTRY="${{ env.REGISTRY }}" IMAGE_NAME="${{ env.IMAGE_NAME }}" # Get image tag from build job output with fallback IMAGE_TAG="${{ needs.build.outputs.image_tag }}" # If IMAGE_TAG is empty, use latest if [ -z "$IMAGE_TAG" ] || [ "$IMAGE_TAG" = "..." ] || [ "$IMAGE_TAG" = "null" ]; then COMMIT_SHA="${{ github.sha }}" if [ -z "$COMMIT_SHA" ]; then COMMIT_SHA=$(cd /workspace/repo && git rev-parse HEAD 2>/dev/null || echo "") fi if [ -z "$COMMIT_SHA" ]; then IMAGE_TAG="latest" else SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) IMAGE_TAG="git-${SHORT_SHA}" fi fi FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" STACK_PATH="~/deployment/stacks/application" echo "🚀 Starting production deployment..." echo " Image: ${FULL_IMAGE}" echo " Tag: ${IMAGE_TAG}" echo " Host: ${DEPLOYMENT_HOST}" echo " Stack: ${STACK_PATH}" ssh -i ~/.ssh/production \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ deploy@${DEPLOYMENT_HOST} <