diff --git a/docker/compose-wait-healthy/action.yaml b/docker/compose-wait-healthy/action.yaml new file mode 100644 index 0000000..5a7c6a2 --- /dev/null +++ b/docker/compose-wait-healthy/action.yaml @@ -0,0 +1,34 @@ +name: compose-wait-healthy +description: "Wait until every named docker-compose service reports 'healthy' (or 'running' when no HEALTHCHECK is defined). On timeout, dumps tail logs for diagnosis." +inputs: + services: + description: "Whitespace-separated list of compose service names to wait on." + required: true + deadline: + description: "Per-invocation deadline in seconds before the action fails." + required: false + default: "600" + poll: + description: "Seconds between polls." + required: false + default: "5" + project: + description: "Compose project name (passed as -p). Optional." + required: false + default: "" + workingDirectory: + description: "Directory containing the compose.yml/compose.yaml. Defaults to repo root." + required: false + default: "." +runs: + using: "composite" + steps: + - name: "Wait for healthy" + shell: bash + working-directory: ${{ inputs.workingDirectory }} + env: + WAIT_HEALTHY_SERVICES: ${{ inputs.services }} + WAIT_HEALTHY_DEADLINE: ${{ inputs.deadline }} + WAIT_HEALTHY_POLL: ${{ inputs.poll }} + COMPOSE_PROJECT: ${{ inputs.project }} + run: bash "$GITHUB_ACTION_PATH/wait-healthy.sh" diff --git a/docker/compose-wait-healthy/wait-healthy.sh b/docker/compose-wait-healthy/wait-healthy.sh new file mode 100644 index 0000000..a58882d --- /dev/null +++ b/docker/compose-wait-healthy/wait-healthy.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# wait-healthy.sh — poll docker compose services until all are healthy/running. +# Invoked by the compose-wait-healthy composite action. Configure via env vars: +# WAIT_HEALTHY_SERVICES whitespace-separated list of service names (required) +# WAIT_HEALTHY_DEADLINE seconds before the script fails (default: 600) +# WAIT_HEALTHY_POLL seconds between polls (default: 5) +# COMPOSE_PROJECT compose project name passed via -p (optional) +set -euo pipefail + +compose() { + if [[ -n "${COMPOSE_PROJECT:-}" ]]; then + docker compose -p "${COMPOSE_PROJECT}" "$@" + else + docker compose "$@" + fi +} + +read -r -a services <<< "${WAIT_HEALTHY_SERVICES}" +if [[ ${#services[@]} -eq 0 ]]; then + echo "ERROR: WAIT_HEALTHY_SERVICES is empty" >&2 + exit 64 +fi + +deadline="${WAIT_HEALTHY_DEADLINE:-600}" +poll="${WAIT_HEALTHY_POLL:-5}" +start="$(date +%s)" + +while :; do + pending=() + for svc in "${services[@]}"; do + cid="$(compose ps -q "${svc}" || true)" + if [[ -z "${cid}" ]]; then + pending+=("${svc}(no container)") + continue + fi + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${cid}" 2>/dev/null || echo unknown)" + case "${status}" in + healthy|running) ;; + *) pending+=("${svc}=${status}") ;; + esac + done + + if [[ ${#pending[@]} -eq 0 ]]; then + echo "all services healthy: ${services[*]}" + exit 0 + fi + + elapsed=$(( $(date +%s) - start )) + if (( elapsed > deadline )); then + echo "FAIL: timeout after ${elapsed}s waiting on: ${pending[*]}" >&2 + for svc in "${services[@]}"; do + echo "----- ${svc} (last 50 log lines) -----" >&2 + compose logs --no-color --tail=50 "${svc}" >&2 || true + done + exit 1 + fi + + printf '[%4ds] waiting on: %s\n' "${elapsed}" "${pending[*]}" + sleep "${poll}" +done