Files
michaelschiemer/.gitea/workflows/build-image.yml
Michael Schiemer 13e12487d7 fix(ci): Fix RUNTIME_IMAGE_NAME variable substitution in runtime-base job
- Added RUNTIME_IMAGE_NAME to env section of Build and push runtime base image step
- Added RUNTIME_IMAGE_NAME to env section of Set runtime base outputs step
- Fixes 'bad substitution' error when variable is used in shell scripts
2025-11-01 21:17:44 +01:00

1080 lines
39 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
default: 'main'
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 || 'main' }}"
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="main"
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; fall back to building to stay safe
echo "⚠️ Keine Änderungsinformation gefunden bilde Image sicherheitshalber."
echo "needs_build=true" >> "$GITHUB_OUTPUT"
echo "changed_files=<none>" >> "$GITHUB_OUTPUT"
echo "needs_runtime_build=true" >> "$GITHUB_OUTPUT"
exit 0
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\.production\.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"
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="<none>"
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
runs-on: docker-build
outputs:
image_ref: ${{ steps.set-result.outputs.image_ref }}
built: ${{ steps.set-result.outputs.built }}
env:
RUNTIME_IMAGE_NAME: ${{ env.RUNTIME_IMAGE_NAME }}
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 || 'main' }}"
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
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
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 || 'main' }}"
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]
if: needs.changes.outputs.needs_build == 'true'
runs-on: docker-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: Install git and setup environment
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
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 || 'main' }}"
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
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
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
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
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
id: runtime-image
shell: bash
run: |
DEFAULT_IMAGE="${{ env.REGISTRY }}/${{ env.RUNTIME_IMAGE_NAME }}:latest"
SELECTED_IMAGE="$DEFAULT_IMAGE"
if [ -n "${{ needs.runtime-base.outputs.image_ref }}" ]; then
SELECTED_IMAGE="${{ needs.runtime-base.outputs.image_ref }}"
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
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"
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 type=registry,ref="${CACHE_TARGET}/${IMAGE_NAME}:buildcache" \
--cache-from type=registry,ref="${REGISTRY_TO_USE}/${IMAGE_NAME}:latest" \
--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} \
--push \
.
echo "✅ Image built and pushed successfully!"
- name: Set image info
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, build, runtime-base]
if: (github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging'))) && needs.changes.outputs.needs_build == 'true'
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="${{ env.REGISTRY }}"
IMAGE_NAME="${{ env.IMAGE_NAME }}"
IMAGE_TAG="latest"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
STACK_PATH="~/deployment/stacks/staging"
echo "🚀 Starting staging 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} <<EOF
set -e
# Ensure staging stack directory exists
mkdir -p ${STACK_PATH}
cd ${STACK_PATH}
echo "🔐 Logging in to Docker registry..."
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${REGISTRY} \
-u "${{ secrets.REGISTRY_USER }}" \
--password-stdin || echo "⚠️ Registry login failed, continuing..."
echo "📥 Pulling image ${FULL_IMAGE}..."
docker pull ${FULL_IMAGE} || {
echo "❌ Failed to pull image ${FULL_IMAGE}"
exit 1
}
# If docker-compose.yml doesn't exist, it will be created from repo
if [ ! -f docker-compose.yml ]; then
echo "⚠️ docker-compose.yml not found, copying from repo..."
cp /workspace/repo/deployment/stacks/staging/docker-compose.yml . || {
echo "❌ Failed to copy docker-compose.yml"
exit 1
}
fi
# Update docker-compose.yml with new image tag
echo "📝 Updating docker-compose.yml..."
sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${FULL_IMAGE}|g" docker-compose.yml
echo "✅ Updated docker-compose.yml:"
grep "image:" docker-compose.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..."
docker compose up -d --pull always --force-recreate || {
echo "❌ Failed to start services"
exit 1
}
echo "⏳ Waiting for services to start..."
sleep 15
# Force containers to pull latest code from Git repository
echo "🔄 Pulling latest code from Git repository in staging-app container..."
docker compose 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 restart staging-app || echo "⚠️ Failed to restart staging-app"
echo "⏳ Waiting for services to stabilize..."
sleep 10
echo "📊 Container status:"
docker compose 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, runtime-base]
if: (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} <<EOF
set -e
cd ${STACK_PATH}
echo "🔐 Logging in to Docker registry..."
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${REGISTRY} \
-u "${{ secrets.REGISTRY_USER }}" \
--password-stdin || echo "⚠️ Registry login failed, continuing..."
echo "📥 Pulling image ${FULL_IMAGE}..."
docker pull ${FULL_IMAGE} || {
echo "❌ Failed to pull image ${FULL_IMAGE}"
exit 1
}
echo "📝 Updating docker-compose.yml..."
sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${FULL_IMAGE}|g" docker-compose.yml
sed -i "s|image:.*/${IMAGE_NAME}@.*|image: ${FULL_IMAGE}|g" docker-compose.yml
echo "✅ Updated docker-compose.yml:"
grep "image:" docker-compose.yml | head -5
echo "🔄 Restarting services..."
docker compose up -d --pull always --force-recreate || {
echo "❌ Failed to restart services"
exit 1
}
echo "⏳ Waiting for services to start..."
sleep 10
echo "📊 Container status:"
docker compose ps
echo "✅ Production 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://michaelschiemer.de/health; then
echo "✅ Health check passed"
exit 0
fi
echo "⏳ Waiting for production service... (attempt $i/10)"
sleep 10
done
echo "❌ Health check failed"
exit 1
- 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' }}"