The deployment script was only copying docker-compose files when missing, preventing configuration updates (like build: null overrides) from being deployed. Changed from conditional copy to always sync latest files from repository, ensuring all configuration changes are properly deployed to production.
1320 lines
50 KiB
YAML
1320 lines
50 KiB
YAML
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
|
||
|
||
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: 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=<none>" >> "$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=<none>" >> "$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="<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 }}
|
||
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
|
||
if: ${{ steps.decision.outputs.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: 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
|
||
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.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
|
||
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
|
||
|
||
# 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
|
||
|
||
# Job 3: Auto-deploy to Staging (only for staging branch)
|
||
deploy-staging:
|
||
name: Auto-deploy to Staging
|
||
needs: [changes, build]
|
||
if: ${{ always() && (github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging'))) && needs.build.result != 'failure' && needs.build.result != 'cancelled' && needs.changes.result != 'failure' && needs.changes.result != 'cancelled' }}
|
||
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} <<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
|
||
}
|
||
|
||
# Always copy latest docker-compose files from repo to ensure configuration updates are deployed
|
||
echo "📋 Syncing docker-compose files from repository..."
|
||
cp /workspace/repo/docker-compose.base.yml . || {
|
||
echo "❌ Failed to copy docker-compose.base.yml"
|
||
exit 1
|
||
}
|
||
|
||
cp /workspace/repo/docker-compose.production.yml . || {
|
||
echo "❌ Failed to copy docker-compose.production.yml"
|
||
exit 1
|
||
}
|
||
|
||
echo "✅ Docker Compose files synced from repository"
|
||
|
||
echo "📝 Updating docker-compose.production.yml with new image tag..."
|
||
sed -i "s|image:.*/${IMAGE_NAME}:.*|image: ${FULL_IMAGE}|g" docker-compose.production.yml
|
||
sed -i "s|image:.*/${IMAGE_NAME}@.*|image: ${FULL_IMAGE}|g" docker-compose.production.yml
|
||
|
||
echo "✅ Updated docker-compose.production.yml:"
|
||
grep "image:" docker-compose.production.yml | head -5
|
||
|
||
echo "🔄 Restarting 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.production.yml up -d --pull missing --force-recreate || {
|
||
echo "❌ Failed to restart services"
|
||
exit 1
|
||
}
|
||
|
||
echo "⏳ Waiting for services to start..."
|
||
sleep 10
|
||
|
||
echo "📊 Container status:"
|
||
docker compose -f docker-compose.base.yml -f docker-compose.production.yml 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' }}"
|