name: 🚀 Build & Deploy Image run-name: Build Image - ${{ github.ref_name }} - ${{ github.sha }} on: push: branches: - main - staging paths: - '.gitea/workflows/**' - 'src/**' - 'resources/**' - 'config/**' - 'public/**' - 'composer.json' - 'composer.lock' - 'package.json' - 'package-lock.json' - 'Dockerfile.production' - 'Dockerfile.runtime' - 'docker-compose.yml' - 'docker-compose.*.yml' - 'docker/**' - 'deployment/**' - 'scripts/ci/**' - 'vite.config.*' - 'tsconfig.json' - 'babel.config.js' - 'Makefile' 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 deploy: description: 'Deploy to staging/production after build (default: false)' type: boolean required: false default: false env: REGISTRY: registry.michaelschiemer.de IMAGE_NAME: framework RUNTIME_IMAGE_NAME: framework-runtime concurrency: group: build-image-${{ github.ref_name || github.head_ref || inputs.branch || github.run_id }} cancel-in-progress: true 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: Upload CI helpers as artifact uses: actions/upload-artifact@v4 with: name: ci-helpers path: /tmp/ci-tools/clone_repo.sh retention-days: 1 - 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" - name: Upload repository as artifact uses: actions/upload-artifact@v4 with: name: repository path: /workspace/repo retention-days: 1 runtime-base: name: Build Runtime Base Image needs: changes runs-on: docker-build outputs: image_ref: ${{ steps.set-result.outputs.image_ref }} built: ${{ steps.set-result.outputs.built }} steps: - 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: Download CI helpers from artifact if: ${{ steps.decision.outputs.should_build == 'true' }} uses: actions/download-artifact@v4 with: name: ci-helpers path: /tmp/ci-tools continue-on-error: true - name: Download CI helpers (fallback if artifact missing) if: ${{ steps.decision.outputs.should_build == 'true' && failure() }} 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: Skip runtime base build if: ${{ steps.decision.outputs.should_build != 'true' }} run: | echo "ℹ️ Runtime base build not required – skipping." - 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 from artifact uses: actions/download-artifact@v4 with: name: ci-helpers path: /tmp/ci-tools continue-on-error: true - name: Download CI helpers (fallback if artifact missing) if: failure() 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: Download repository artifact uses: actions/download-artifact@v4 with: name: repository path: /workspace continue-on-error: true id: download_repo - name: Checkout code (fallback if artifact missing) if: steps.download_repo.outcome == 'failure' 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.image_info.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 from artifact if: ${{ env.SHOULD_BUILD == 'true' }} uses: actions/download-artifact@v4 with: name: ci-helpers path: /tmp/ci-tools continue-on-error: true - name: Download CI helpers (fallback if artifact missing) if: ${{ env.SHOULD_BUILD == 'true' && failure() }} 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: Download repository artifact if: ${{ env.SHOULD_BUILD == 'true' }} uses: actions/download-artifact@v4 with: name: repository path: /workspace continue-on-error: true id: download_repo - name: Checkout code (fallback if artifact missing) if: ${{ env.SHOULD_BUILD == 'true' && steps.download_repo.outcome == 'failure' }} 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 # Build image with cache but don't push yet echo "🏗️ Building image..." 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 \ --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ --build-arg GIT_COMMIT=${COMMIT_SHA} \ --build-arg GIT_BRANCH=${REF_NAME} \ --load \ . echo "✅ Image built successfully!" # Push tags one by one to avoid overwhelming the registry echo "📤 Pushing tags to registry..." docker tag "${REGISTRY_TO_USE}/${IMAGE_NAME}:latest" "${REGISTRY_TO_USE}/${IMAGE_NAME}:${TAG}" docker tag "${REGISTRY_TO_USE}/${IMAGE_NAME}:latest" "${REGISTRY_TO_USE}/${IMAGE_NAME}:git-${SHORT_SHA}" echo " Pushing latest..." docker push "${REGISTRY_TO_USE}/${IMAGE_NAME}:latest" echo " Pushing ${TAG}..." docker push "${REGISTRY_TO_USE}/${IMAGE_NAME}:${TAG}" echo " Pushing git-${SHORT_SHA}..." docker push "${REGISTRY_TO_USE}/${IMAGE_NAME}:git-${SHORT_SHA}" echo "✅ All tags pushed successfully!" - name: Set image info id: image_info shell: bash env: SHOULD_BUILD: ${{ env.SHOULD_BUILD }} REF_NAME: ${{ github.ref_name }} HEAD_REF: ${{ github.head_ref }} FULL_REF: ${{ github.ref }} run: | REGISTRY_DEFAULT="$REGISTRY" if [ -z "$REGISTRY_DEFAULT" ]; then REGISTRY_DEFAULT="${{ env.REGISTRY }}" fi IMAGE_NAME="${{ env.IMAGE_NAME }}" COMMIT_SHA="${{ github.sha }}" if [ -z "$COMMIT_SHA" ]; then COMMIT_SHA="unknown" fi if [ "${SHOULD_BUILD}" != "true" ]; then IMAGE_TAG="latest" IMAGE_URL="${REGISTRY_DEFAULT}/${IMAGE_NAME}:${IMAGE_TAG}" echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_OUTPUT" echo "IMAGE_URL=$IMAGE_URL" >> "$GITHUB_OUTPUT" echo "COMMIT_SHA=$COMMIT_SHA" >> "$GITHUB_OUTPUT" echo "ℹ️ Build skipped – using latest image tag ($IMAGE_TAG)." EFFECTIVE_REF="$REF_NAME" if [ -z "$EFFECTIVE_REF" ]; then EFFECTIVE_REF="$HEAD_REF" fi if [ -z "$EFFECTIVE_REF" ] && [ -n "$FULL_REF" ]; then EFFECTIVE_REF="${FULL_REF##*/}" fi if [ "$EFFECTIVE_REF" = "staging" ]; then echo "🚀 Staging branch detected - will auto-deploy using the latest published image." else echo "💡 Image is ready for deployment!" echo " Run the 'Deploy to Production' or 'Deploy to Staging' workflow to deploy this image." fi exit 0 fi cd /workspace/repo if [ -z "$COMMIT_SHA" ] || [ "$COMMIT_SHA" = "unknown" ]; then COMMIT_SHA=$(git rev-parse HEAD) fi SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) TAG="${SHORT_SHA}-$(date +%s)" REGISTRY_TO_USE="${REGISTRY_URL:-$REGISTRY_DEFAULT}" # Use git-SHA format for deployment (stable, doesn't change with time) IMAGE_TAG="git-${SHORT_SHA}" IMAGE_URL="${REGISTRY_TO_USE}/${IMAGE_NAME}:${IMAGE_TAG}" echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_OUTPUT" echo "IMAGE_URL=$IMAGE_URL" >> "$GITHUB_OUTPUT" echo "COMMIT_SHA=$COMMIT_SHA" >> "$GITHUB_OUTPUT" echo "📦 Image info:" echo " Tag: $IMAGE_TAG (stable git-based tag)" echo " Also pushed: $TAG (timestamped), latest" echo " URL: $IMAGE_URL" echo "" REF_NAME="${{ github.ref_name }}" if [ -z "$REF_NAME" ]; then REF_NAME=$(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 - name: Upload repository as artifact if: ${{ env.SHOULD_BUILD == 'true' }} uses: actions/upload-artifact@v4 with: name: repository path: /workspace/repo retention-days: 1 # Job 3: Auto-deploy to Staging (only for staging branch and if deploy is enabled) deploy-staging: name: Auto-deploy to Staging needs: [changes, build] if: ${{ always() && ((github.event_name == 'push' && (github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging')))) || (github.event_name == 'workflow_dispatch' && inputs.deploy == true)) && needs.build.result != 'failure' && needs.build.result != 'cancelled' && needs.changes.result != 'failure' && needs.changes.result != 'cancelled' }} runs-on: php-ci concurrency: group: deploy-staging cancel-in-progress: false 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: Download repository artifact uses: actions/download-artifact@v4 with: name: repository path: /workspace continue-on-error: true id: download_repo - name: Checkout deployment scripts (fallback if artifact missing) if: steps.download_repo.outcome == 'failure' 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: Set skip_git_update flag if repository artifact was used if: steps.download_repo.outcome == 'success' run: | echo "SKIP_GIT_UPDATE=true" >> $GITHUB_ENV - 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: Create Ansible Vault password file run: | if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass chmod 600 /tmp/vault_pass echo "✅ Vault password file created" else echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file" touch /tmp/vault_pass chmod 600 /tmp/vault_pass fi - name: Deploy to Staging (Complete) run: | cd /workspace/repo/deployment/ansible ansible-playbook -i inventory/production.yml \ playbooks/deploy-complete.yml \ -e "deployment_environment=staging" \ -e "deployment_hosts=production" \ -e "git_branch=staging" \ -e "image_tag=latest" \ -e "docker_registry=${{ env.REGISTRY }}" \ -e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \ -e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \ -e "application_skip_git_update=${{ env.SKIP_GIT_UPDATE || 'false' }}" \ -e "traefik_auto_restart=false" \ -e "gitea_auto_restart=false" \ --vault-password-file /tmp/vault_pass \ --private-key ~/.ssh/production - name: Wait for deployment to stabilize run: sleep 30 - name: Health check id: health run: | echo "🔍 Performing health checks with exponential backoff..." # Basic health check with exponential backoff BASIC_HEALTH_OK=false DELAY=2 MAX_DELAY=60 MAX_ATTEMPTS=5 for i in $(seq 1 $MAX_ATTEMPTS); do if curl -f -k -s https://staging.michaelschiemer.de/health > /dev/null 2>&1; then echo "✅ Basic health check passed (attempt $i/$MAX_ATTEMPTS)" BASIC_HEALTH_OK=true break fi if [ $i -lt $MAX_ATTEMPTS ]; then echo "⏳ Waiting for staging service... (attempt $i/$MAX_ATTEMPTS, delay ${DELAY}s)" sleep $DELAY DELAY=$((DELAY * 2)) [ $DELAY -gt $MAX_DELAY ] && DELAY=$MAX_DELAY fi done if [ "$BASIC_HEALTH_OK" != "true" ]; then echo "❌ Basic health check failed after $MAX_ATTEMPTS attempts" exit 1 fi # Extended health check (if available) echo "🔍 Checking extended health status..." HEALTH_SUMMARY=$(curl -f -k -s https://staging.michaelschiemer.de/admin/health/api/summary 2>/dev/null || echo "") if [ -n "$HEALTH_SUMMARY" ]; then OVERALL_STATUS=$(echo "$HEALTH_SUMMARY" | grep -o '"overall_status":"[^"]*"' | cut -d'"' -f4 || echo "unknown") echo "📊 Overall health status: $OVERALL_STATUS" if [ "$OVERALL_STATUS" = "unhealthy" ]; then echo "⚠️ Extended health check shows unhealthy status" echo " Health summary: $HEALTH_SUMMARY" else echo "✅ Extended health check passed" fi else echo "ℹ️ Extended health check endpoint not available (this is OK)" fi echo "✅ All health checks completed" - 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 and if deploy is enabled) deploy-production: name: Auto-deploy to Production needs: [changes, build] if: always() && ((github.event_name == 'push' && (github.ref_name == 'main' || github.head_ref == 'main' || (github.ref_name == '' && contains(github.ref, 'main')))) || (github.event_name == 'workflow_dispatch' && inputs.deploy == true)) && needs.changes.outputs.needs_build == 'true' runs-on: php-ci concurrency: group: deploy-production cancel-in-progress: false 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: Download repository artifact uses: actions/download-artifact@v4 with: name: repository path: /workspace continue-on-error: true id: download_repo - name: Checkout deployment scripts (fallback if artifact missing) if: steps.download_repo.outcome == 'failure' 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: Set skip_git_update flag if repository artifact was used if: steps.download_repo.outcome == 'success' run: | echo "SKIP_GIT_UPDATE=true" >> $GITHUB_ENV - 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: Create Ansible Vault password file run: | if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass chmod 600 /tmp/vault_pass echo "✅ Vault password file created" else echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file" touch /tmp/vault_pass chmod 600 /tmp/vault_pass fi - name: Determine image tag id: image_tag run: | # 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 echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_OUTPUT echo "📦 Image Tag: ${IMAGE_TAG}" - name: Deploy to Production (Complete) run: | cd /workspace/repo/deployment/ansible ansible-playbook -i inventory/production.yml \ playbooks/deploy-complete.yml \ -e "deployment_environment=production" \ -e "deployment_hosts=production" \ -e "git_branch=main" \ -e "image_tag=${{ steps.image_tag.outputs.IMAGE_TAG }}" \ -e "docker_registry=${{ env.REGISTRY }}" \ -e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \ -e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \ -e "application_skip_git_update=${{ env.SKIP_GIT_UPDATE || 'false' }}" \ -e "traefik_auto_restart=false" \ -e "gitea_auto_restart=false" \ --vault-password-file /tmp/vault_pass \ --private-key ~/.ssh/production - name: Wait for deployment to stabilize run: sleep 30 - name: Health check id: health run: | echo "🔍 Performing health checks with exponential backoff..." # Basic health check with exponential backoff BASIC_HEALTH_OK=false DELAY=2 MAX_DELAY=60 MAX_ATTEMPTS=5 for i in $(seq 1 $MAX_ATTEMPTS); do if curl -f -k -s https://michaelschiemer.de/health > /dev/null 2>&1; then echo "✅ Basic health check passed (attempt $i/$MAX_ATTEMPTS)" BASIC_HEALTH_OK=true break fi if [ $i -lt $MAX_ATTEMPTS ]; then echo "⏳ Waiting for production service... (attempt $i/$MAX_ATTEMPTS, delay ${DELAY}s)" sleep $DELAY DELAY=$((DELAY * 2)) [ $DELAY -gt $MAX_DELAY ] && DELAY=$MAX_DELAY fi done if [ "$BASIC_HEALTH_OK" != "true" ]; then echo "❌ Basic health check failed after $MAX_ATTEMPTS attempts" exit 1 fi # Extended health check (if available) echo "🔍 Checking extended health status..." HEALTH_SUMMARY=$(curl -f -k -s https://michaelschiemer.de/admin/health/api/summary 2>/dev/null || echo "") if [ -n "$HEALTH_SUMMARY" ]; then OVERALL_STATUS=$(echo "$HEALTH_SUMMARY" | grep -o '"overall_status":"[^"]*"' | cut -d'"' -f4 || echo "unknown") echo "📊 Overall health status: $OVERALL_STATUS" if [ "$OVERALL_STATUS" = "unhealthy" ]; then echo "⚠️ Extended health check shows unhealthy status" echo " Health summary: $HEALTH_SUMMARY" else echo "✅ Extended health check passed" fi else echo "ℹ️ Extended health check endpoint not available (this is OK)" fi echo "✅ All health checks completed" - name: Notify deployment success if: success() run: | echo "🚀 Production deployment successful!" echo "URL: https://michaelschiemer.de" echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }}"