diff --git a/.gitea/workflows/manual-deploy.yml b/.gitea/workflows/manual-deploy.yml new file mode 100644 index 00000000..aa8c2656 --- /dev/null +++ b/.gitea/workflows/manual-deploy.yml @@ -0,0 +1,456 @@ +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} <