From 3d8207af0fc58a9b84aa75411595e379a14b6b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Mon, 11 May 2026 21:39:22 +0200 Subject: [PATCH] ci(preview-env): replace bore.pub with Cloudflare quick tunnel (#20459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `bore.pub`'s public server has been increasingly unreliable: tunnels register fine on the runner side (our `Create Tunnel` step always succeeds), but the bore.pub side later stops accepting inbound traffic, leaving the preview environment unreachable for the rest of the 5h keep-alive window with no signal back to the runner. Recent symptom: `curl http://bore.pub:50422` → `Couldn't connect to server`, while the corresponding action keeps sleeping. This PR replaces the `codetalkio/expose-tunnel` action with a direct invocation of `cloudflared` running an account-less [Cloudflare quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/). The tunnel is served from Cloudflare's edge so reliability is materially better, and the URL is HTTPS by default (`https://*.trycloudflare.com`), which also eliminates the mixed-content issues we'd hit when `SERVER_URL` was `http://bore.pub:port`. ## What changes - `Create Tunnel` step now: - Downloads a pinned `cloudflared` binary (`2026.3.0`) - Starts `cloudflared tunnel --url http://localhost:3000` in the background, logging to `$RUNNER_TEMP/cloudflared.log` - Polls the log for `https://.trycloudflare.com` (up to 2 minutes), failing fast if the process exits - Writes the URL to the `tunnel-url` step output — same name as before, so no downstream changes needed - `Cleanup` step kills the `cloudflared` process for hygiene ## What stays the same - `SERVER_URL` plumbing through `.env` → `docker compose up` - `tunnel-url` artifact - `$GITHUB_STEP_SUMMARY` formatting - PR-comment dispatch (`twentyhq/ci-privileged`) - 5h keep-alive sleep ## Trade-offs - Quick tunnels are explicitly labelled by Cloudflare for "testing/development" use without an SLA. For our preview-env use case (ephemeral, per-PR) that fits, but if we ever need stable URLs on a custom domain we'd move to *named* tunnels — same `cloudflared` binary, plus a free Cloudflare account + delegated domain + a service token stored as a repo secret. Strictly additive when we want it. - `cloudflared` is pinned to `2026.3.0` to avoid surprise breakage from upstream releases. Bumping is a one-line change. ## Testing **Locally (macOS) — verified end-to-end:** - `cloudflared tunnel --url http://localhost:18080` against a `python3 -m http.server` - Regex `https://[a-zA-Z0-9-]+\.trycloudflare\.com` correctly extracts the URL from the log - `curl $URL/` returns the upstream server's response (HTTP 200, ~0.5s) - Process supervision: if `cloudflared` dies mid-wait, the step fails fast instead of hitting the 2-min timeout **Validation:** - `actionlint` passes (the remaining shellcheck warnings are in pre-existing steps, not my changes) - `shellcheck` on the new Create Tunnel script: clean **What's not testable from a PR (and why):** - The full keep-alive workflow runs on `repository_dispatch`, which always uses the workflow file from `main`. So the cloudflared logic only runs against PR contents *after* merge. - I'll trigger a one-off Ubuntu-runner test of just the install + URL extraction logic via a throwaway branch (`workflow_dispatch`-only) and link the run here before this merges. ## Test plan - [ ] Throwaway run validates: cloudflared installs on `ubuntu-latest`, prints the URL, regex matches, tunnel is reachable from outside the runner. - [ ] After merge, the next PR's preview environment uses `*.trycloudflare.com` instead of `bore.pub:port`, and the URL stays reachable for the full 5h window. - [ ] PR-comment bot still posts the preview URL correctly (link should now be `https://*.trycloudflare.com`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 --- .github/workflows/preview-env-keepalive.yaml | 55 ++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview-env-keepalive.yaml b/.github/workflows/preview-env-keepalive.yaml index 590c33073e..2a986a63cd 100644 --- a/.github/workflows/preview-env-keepalive.yaml +++ b/.github/workflows/preview-env-keepalive.yaml @@ -56,10 +56,54 @@ jobs: - name: Create Tunnel id: expose-tunnel - uses: codetalkio/expose-tunnel@v1.5.0 - with: - service: bore.pub - port: 3000 + env: + CLOUDFLARED_VERSION: '2026.3.0' + run: | + set -euo pipefail + + # Install cloudflared (pinned for reproducibility) + sudo curl -fsSL -o /usr/local/bin/cloudflared \ + "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" + sudo chmod +x /usr/local/bin/cloudflared + cloudflared --version + + # Start an account-less "quick tunnel" pointing at the server container. + # Cloudflare prints the assigned https://*.trycloudflare.com URL into the log. + log_file="$RUNNER_TEMP/cloudflared.log" + : > "$log_file" + + cloudflared tunnel \ + --url http://localhost:3000 \ + --no-autoupdate \ + --logfile "$log_file" \ + --loglevel info \ + > "$RUNNER_TEMP/cloudflared.stdout" 2>&1 & + + pid=$! + echo "$pid" > "$RUNNER_TEMP/cloudflared.pid" + echo "cloudflared PID: $pid" + + # Wait up to 2 minutes for the URL to appear; fail fast if cloudflared exits. + url='' + for _ in $(seq 1 60); do + url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$log_file" 2>/dev/null | head -n1 || true) + [ -n "$url" ] && break + if ! kill -0 "$pid" 2>/dev/null; then + echo "cloudflared exited before producing a URL" + cat "$log_file" || true + exit 1 + fi + sleep 2 + done + + if [ -z "$url" ]; then + echo "Timed out waiting for tunnel URL" + cat "$log_file" || true + exit 1 + fi + + echo "Tunnel URL: $url" + echo "tunnel-url=$url" >> "$GITHUB_OUTPUT" - name: Start services with correct SERVER_URL env: @@ -134,6 +178,9 @@ jobs: - name: Cleanup if: always() run: | + if [ -f "$RUNNER_TEMP/cloudflared.pid" ]; then + kill "$(cat "$RUNNER_TEMP/cloudflared.pid")" 2>/dev/null || true + fi cd packages/twenty-docker/ docker compose down -v working-directory: ./