name: 🚀 Manual Deployment run-name: Manual Deploy - ${{ inputs.environment }} - ${{ inputs.image_tag || 'latest' }} on: workflow_dispatch: inputs: environment: description: 'Deployment environment' required: true type: choice options: - staging - production image_tag: description: 'Image tag to deploy (e.g. abc1234-1696234567, git-abc1234). Leave empty for latest' required: false type: string default: '' branch: description: 'Branch to checkout (default: main for production, staging for staging)' required: false type: string default: '' env: REGISTRY: registry.michaelschiemer.de IMAGE_NAME: framework DEPLOYMENT_HOST: 94.16.110.151 jobs: determine-image: name: Determine Deployment Image runs-on: ubuntu-latest outputs: image_url: ${{ steps.image.outputs.image_url }} image_tag: ${{ steps.image.outputs.image_tag }} registry_host: ${{ env.REGISTRY }} image_name: ${{ env.IMAGE_NAME }} steps: - name: Determine image to deploy id: image shell: bash run: | REGISTRY="${{ env.REGISTRY }}" IMAGE_NAME="${{ env.IMAGE_NAME }}" INPUT_TAG="${{ inputs.image_tag }}" if [ -z "$INPUT_TAG" ] || [ "$INPUT_TAG" = "" ]; then IMAGE_TAG="latest" else IMAGE_TAG="$INPUT_TAG" fi IMAGE_URL="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" echo "image_url=${IMAGE_URL}" >> "$GITHUB_OUTPUT" echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" echo "đŸ“Ļ Deployment Image:" echo " URL: ${IMAGE_URL}" echo " Tag: ${IMAGE_TAG}" echo "" echo "â„šī¸ Image will be validated during deployment" deploy-staging: name: Deploy to Staging needs: determine-image if: inputs.environment == 'staging' runs-on: ubuntu-latest environment: name: staging url: https://staging.michaelschiemer.de steps: - name: Determine branch name id: branch shell: bash run: | INPUT_BRANCH="${{ inputs.branch }}" if [ -z "$INPUT_BRANCH" ] || [ "$INPUT_BRANCH" = "" ]; then REF_NAME="staging" else REF_NAME="$INPUT_BRANCH" 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="${{ needs.determine-image.outputs.registry_host }}" IMAGE_NAME="${{ needs.determine-image.outputs.image_name }}" DEPLOY_IMAGE="${{ needs.determine-image.outputs.image_url }}" IMAGE_TAG="${{ needs.determine-image.outputs.image_tag }}" DEFAULT_IMAGE="${REGISTRY_HOST}/${IMAGE_NAME}:latest" FALLBACK_IMAGE="$DEFAULT_IMAGE" SELECTED_IMAGE="$DEPLOY_IMAGE" if [ -z "$SELECTED_IMAGE" ] || [ "$SELECTED_IMAGE" = "null" ]; then SELECTED_IMAGE="$DEFAULT_IMAGE" fi 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 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: ${{ needs.determine-image.outputs.image_url }}" deploy-production: name: Deploy to Production needs: determine-image if: inputs.environment == 'production' runs-on: ubuntu-latest environment: name: production url: https://michaelschiemer.de steps: - name: Determine branch name id: branch shell: bash run: | INPUT_BRANCH="${{ inputs.branch }}" if [ -z "$INPUT_BRANCH" ] || [ "$INPUT_BRANCH" = "" ]; then REF_NAME="main" else REF_NAME="$INPUT_BRANCH" 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="${{ needs.determine-image.outputs.registry_host }}" IMAGE_NAME="${{ needs.determine-image.outputs.image_name }}" IMAGE_TAG="${{ needs.determine-image.outputs.image_tag }}" 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} <