name: Reusable Deploy on: workflow_call: inputs: image_ref: required: true type: string service_name: required: true type: string timeout: required: false type: string default: '120' secrets: SSH_HOST: required: true SSH_USER: required: true SSH_PRIVATE_KEY: required: true jobs: deploy: runs-on: docker-build steps: - name: Deploy uses: appleboy/ssh-action@v1.2.0 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} command_timeout: ${{ inputs.timeout }}m script: | echo "🚀 Updating service ${{ inputs.service_name }} with image ${{ inputs.image_ref }}..." docker service update \ --force \ --image ${{ inputs.image_ref }} \ ${{ inputs.service_name }} echo "⏳ Waiting for service to converge..." timeout ${{ inputs.timeout }} docker service ls --filter "name=${{ inputs.service_name }}" > /dev/null # Wait for rollout to complete ELAPSED=0 INTERVAL=5 while true; do REPLICAS=$(docker service ls --filter "name=${{ inputs.service_name }}" --format "{{.Replicas}}") RUNNING=$(echo "$REPLICAS" | cut -d'/' -f1) DESIRED=$(echo "$REPLICAS" | cut -d'/' -f2) echo " Replicas: $REPLICAS" if [ "$RUNNING" = "$DESIRED" ] && [ "$DESIRED" != "0" ]; then echo "✅ Service ${{ inputs.service_name }} is running ($REPLICAS)" break fi ELAPSED=$((ELAPSED + INTERVAL)) if [ "$ELAPSED" -ge "${{ inputs.timeout }}" ]; then echo "❌ Timeout: service did not converge in ${{ inputs.timeout }}s" echo "--- Service status ---" docker service ps ${{ inputs.service_name }} --no-trunc exit 1 fi sleep $INTERVAL done # Final check: make sure no tasks are in failed state FAILED=$(docker service ps ${{ inputs.service_name }} --filter "desired-state=shutdown" --format "{{.CurrentState}}" | grep -c "Failed" || true) if [ "$FAILED" -gt "0" ]; then echo "❌ Service has $FAILED failed task(s):" docker service ps ${{ inputs.service_name }} --no-trunc exit 1 fi echo "🎉 Deployment successful!"