457 lines
18 KiB
YAML
457 lines
18 KiB
YAML
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} <<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
|
||
}
|
||
|
||
# Copy base and production 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.production.yml ]; then
|
||
echo "⚠️ docker-compose.production.yml not found, copying from repo..."
|
||
cp /workspace/repo/docker-compose.production.yml . || {
|
||
echo "❌ Failed to copy docker-compose.production.yml"
|
||
exit 1
|
||
}
|
||
fi
|
||
|
||
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: ${{ needs.determine-image.outputs.image_url }}"
|
||
|