From 1fac69479c6bb3fafddc51c2249824ac4eb6a41f Mon Sep 17 00:00:00 2001 From: mignot Date: Sat, 8 Aug 2026 20:24:17 +0200 Subject: [PATCH] fix: bypass Cloudflare Bot Fight Mode in trigger-coolify-deploy health check Cloudflare's Bot Fight Mode issues an instant JS challenge (HTTP 403) to GitHub Actions runner IPs on any Cloudflare-proxied hostname, and it runs outside the Ruleset Engine so no Custom Rule can exempt it. Add health_check_origin_ip so callers can curl --resolve the app's real origin IP directly, skipping the proxy for just the health-check request. --- .../actions/trigger-coolify-deploy/action.yml | 25 +++++++++++++++++-- CHANGELOG.md | 4 +++ README.md | 2 ++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/actions/trigger-coolify-deploy/action.yml b/.github/actions/trigger-coolify-deploy/action.yml index 1ba0bcd..abe18eb 100644 --- a/.github/actions/trigger-coolify-deploy/action.yml +++ b/.github/actions/trigger-coolify-deploy/action.yml @@ -56,6 +56,17 @@ inputs: preview's generated fqdn. required: false default: "" + health_check_origin_ip: + description: >- + Origin server IP to connect the health check directly to (via curl --resolve), bypassing + any reverse proxy in front of the health-check hostname. Required for hostnames proxied + through Cloudflare: Cloudflare's Bot Fight Mode issues an instant JS challenge (HTTP 403) + to GitHub Actions runner IPs on any proxied hostname, and it runs outside the Ruleset + Engine, so no Cloudflare Custom Rule can exempt it — the only way to reach the app's real + health check is to skip the proxy for this one request. TLS still validates normally + since the hostname (SNI/Host) is unchanged, only the connect-to address is overridden. + required: false + default: "" runs: using: composite @@ -75,6 +86,7 @@ runs: PREVIEW_NOT_FOUND_TIMEOUT_SECONDS: ${{ inputs.preview_not_found_timeout_seconds }} HEALTH_CHECK_PATH: ${{ inputs.health_check_path }} HEALTH_CHECK_BASE_URL_OVERRIDE: ${{ inputs.health_check_base_url_override }} + HEALTH_CHECK_ORIGIN_IP: ${{ inputs.health_check_origin_ip }} run: | set -euo pipefail @@ -207,10 +219,19 @@ runs: fi health_url="${base_url%/}${HEALTH_CHECK_PATH}" - echo "Health-checking ${health_url} (timeout ${POLL_TIMEOUT_SECONDS}s)..." + + resolve_opts=() + if [ -n "$HEALTH_CHECK_ORIGIN_IP" ]; then + health_host=$(printf '%s' "$base_url" | sed -E 's#^[a-zA-Z]+://##; s#[/:].*##') + resolve_opts=(--resolve "${health_host}:443:${HEALTH_CHECK_ORIGIN_IP}") + echo "Health-checking ${health_url} directly via ${HEALTH_CHECK_ORIGIN_IP} (bypassing any proxy in front of ${health_host}), timeout ${POLL_TIMEOUT_SECONDS}s..." + else + echo "Health-checking ${health_url} (timeout ${POLL_TIMEOUT_SECONDS}s)..." + fi + start=$(date +%s) while :; do - code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$health_url" || true) + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "${resolve_opts[@]}" "$health_url" || true) if [ "$code" = "200" ]; then echo "Health check passed." break diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dcd3b9..b0acaec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,10 @@ All contributors (including maintainers) should update `CHANGELOG.md` when creat ## [Unreleased] +### Fixed + +- **trigger-coolify-deploy**: added `health_check_origin_ip` input so the health check can `curl --resolve` the app's origin IP directly instead of going through Cloudflare. Cloudflare's Bot Fight Mode issues an instant JS challenge (HTTP 403) to GitHub Actions runner IPs on any Cloudflare-proxied hostname, and it runs outside the Ruleset Engine so no Custom Rule can exempt it — without this, health checks against proxied staging/prod hostnames always fail even though the deployment itself succeeded. + ## [4.2.0] - 2026-08-08 diff --git a/README.md b/README.md index 874b561..8f88a27 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ App-agnostic composite action: trigger a Coolify application deploy via API, pol | `preview_not_found_timeout_seconds`| No | Max seconds to retry a PR-preview deploy trigger while Coolify hasn't created the preview yet. Default `180` | | `health_check_path` | No | Path (e.g. `/health`) to poll for HTTP 200 after the deployment finishes | | `health_check_base_url_override` | No | Base URL to health-check instead of the app's own fqdn. Required when both `pr_number` and `health_check_path` are set (Coolify's API doesn't expose a preview's fqdn) | +| `health_check_origin_ip` | No | Origin server IP to `curl --resolve` the health check directly to, bypassing any reverse proxy in front of the hostname. Needed for Cloudflare-proxied hostnames: Bot Fight Mode 403s GitHub Actions runner IPs on any proxied hostname and runs outside the Ruleset Engine, so no Custom Rule can exempt it | ```yaml - name: Trigger tmd-admin-api deploy @@ -122,6 +123,7 @@ App-agnostic composite action: trigger a Coolify application deploy via API, pol domain: ${{ vars.DOMAIN_NAME }} coolify_api_token: ${{ secrets.COOLIFY_API_TOKEN }} health_check_path: /health + health_check_origin_ip: ${{ vars.SERVER_HOST }} # bypass Cloudflare proxy (Bot Fight Mode 403s CI runners) ``` ### Deploy App Env File