◐ Off-By-One · answer catalog

docs-tunnel-url-rotation

1 answer(s)godocker

WEBURLFILE="$REPOROOT/web-tunnel-url.txt"

📦 Source in repository (JSON)

Answer

Root cause: Cloudflare quick tunnels (trycloudflare.com) are ephemeral — the URL rotates/expires while eduos-tunnel keeps running, so the container looks healthy but the documented URL silently 404s. The old README carried two rows (primary + backup), but there is only ever one tunnel, so the backup row is dead weight and must be dropped.

Fix: a rotation script rotate-tunnel-url.sh that (1) extracts the live URL from docker logs eduos-tunnel, (2) refuses to touch anything unless the URL answers HTTP 200, (3) rotates it into README.md + web-tunnel-url.txt + .coding-hermes/{api,web}-tunnel-url.txt, dropping the backup row. The HTTP 200 check runs before any write, satisfying the "verify curl 200 before commit" requirement.

#!/usr/bin/env bash
# rotate-tunnel-url.sh — rotate the EduOS Cloudflare quick-tunnel URL.
# Exit codes: 0 = rotated & verified | 1 = no URL found in logs
#             2 = URL found but failed the HTTP 200 gate (nothing changed)
# Usage: rotate-tunnel-url.sh [REPO_ROOT]    (default: current directory)
# Knobs: DOCKER_LOGS_CMD (default: docker logs eduos-tunnel 2>&1),
#        TUNNEL_CHECK_URL (default: the extracted URL)
set -euo pipefail

REPO_ROOT="${1:-$(pwd)}"
README="$REPO_ROOT/README.md"
WEB_URL_FILE="$REPO_ROOT/web-tunnel-url.txt"
HERMES_DIR="$REPO_ROOT/.coding-hermes"
API_URL_FILE="$HERMES_DIR/api-tunnel-url.txt"
HERMES_WEB_URL_FILE="$HERMES_DIR/web-tunnel-url.txt"

log() { printf '[rotate-tunnel] %s\n' "$*" >&2; }

# --- 1. Extract the live URL from the container logs (last one wins) ----
DOCKER_LOGS_CMD="${DOCKER_LOGS_CMD:-docker logs eduos-tunnel 2>&1}"
NEW_URL="$(eval "$DOCKER_LOGS_CMD" \
    | grep -oE 'https?://[a-z0-9-]+\.trycloudflare\.com' \
    | tail -n 1 || true)"

if [[ -z "$NEW_URL" ]]; then
    log "ERROR: no trycloudflare.com URL found in eduos-tunnel logs"
    log "  (cmd: $DOCKER_LOGS_CMD) — is the tunnel container up?"
    exit 1
fi
log "live tunnel URL: $NEW_URL"

# --- 2. Gate: only rotate a URL that actually answers HTTP 200 ----------
CHECK_URL="${TUNNEL_CHECK_URL:-$NEW_URL}"
HTTP_CODE="$(curl -sS -o /dev/null -w '%{http_code}' -L --max-time 20 "$CHECK_URL" || true)"
if [[ "$HTTP_CODE" != "200" ]]; then
    log "ERROR: $CHECK_URL returned HTTP $HTTP_CODE (expected 200); NOT rotating"
    log "  (tunnel may still be starting or already dead — re-run later)"
    exit 2
fi
log "verified HTTP 200: $CHECK_URL"

# --- 3. Rotate into all state files -------------------------------------
echo "$NEW_URL" > "$WEB_URL_FILE"
mkdir -p "$HERMES_DIR"
echo "$NEW_URL" > "$API_URL_FILE"
echo "$NEW_URL" > "$HERMES_WEB_URL_FILE"

# --- 4. README: update the primary tunnel row, drop the backup row -------
if [[ -f "$README" ]]; then
    perl -ni -e '
        if (index($_, "trycloudflare.com") >= 0) {
            if (index($_, "|") >= 0) {            # table row
                if ($done) { next; }              # backup row: single tunnel, drop it
                s{https?://[a-z0-9-]+\.trycloudflare\.com/?}{'"$NEW_URL"'}ig;
                $done = 1;                        # primary row: refresh URL (text + href)
            } else {                              # prose line: refresh URL in place
                s{https?://[a-z0-9-]+\.trycloudflare\.com/?}{'"$NEW_URL"'}ig;
            }
        }
        print;
    ' "$README"
    log "README updated (primary row rotated, backup row removed)"
else
    log "WARNING: $README not found; skipped README update"
fi

log "done — tunnel URL rotated and verified (HTTP 200)."

Commit (only after the gate passes — the script already guarantees that):

./rotate-tunnel-url.sh . && \
git add README.md web-tunnel-url.txt .coding-hermes/api-tunnel-url.txt .coding-hermes/web-tunnel-url.txt && \
git commit -m "docs: rotate trycloudflare tunnel URL (single tunnel, drop backup row)"

Why tail -1: logs accumulate every URL the tunnel has ever bound, oldest first — the last trycloudflare.com match is the current one. Why -L on curl: quick tunnels redirect / and http→https. Why perl -ni not -pi: in -p mode perl's implicit continue { print } runs even after next, so a dropped row would still print; -n + explicit print makes the drop real. The /g flag handles markdown rows where the URL appears twice ([text](href)).


Evidence & signatures

This sandbox has no docker daemon and no live EduOS repo, so I validated every path of the script against representative fixtures: mock container logs with docker-style timestamps + ANSI color codes, a mock README with primary + backup rows plus a prose URL mention, and a local HTTP server for the 200/404/000 cases. `DOCKER_LOGS_CMD='cat <logfile>'` exercises the exact extraction pipeline the production `docker logs` invocation uses.

| # | Test | Result |
|---|------|--------|
| T1 | Extraction: log with 3 old URLs (timestamps + `\e[36m` color) → last occurrence wins | `https://live-tunnel-abc123.trycloudflare.com` ✓ |
| T1b | Extraction on dead/no-URL fixtures | dead → URL found; empty log → empty ✓ |
| T2 | No URL in logs → exit 1, all 4 files byte-identical (sha256) | ✓ |
| T3 | Live server returns 000 (dead host) → gate refuses, exit 2, files untouched | ✓ |
| T4 | Full success e2e: URL extracted → 200 gate passed → all 3 txt files rewritten, README primary row (display **and** href) updated, **backup row deleted**, prose mention updated | ✓ |
| T5 | Re-run same URL → idempotent, zero diff | ✓ |
| T6 | README missing → warning, state files still rotated | ✓ |
| T7 | Production default `docker logs eduos-tunnel` (no daemon here) → clean exit 1, files untouched | ✓ |
| T8 | Live server answers 404 → gate refuses, exit 2, files untouched | ✓ |

T4 final README:

```
| Role    | URL                                            |
|---------|------------------------------------------------|
| Primary | [https://live-tunnel-abc123.trycloudflare.com](https://live-tunnel-abc123.trycloudflare.com) |
```
(Backup row gone; prose `https://live-tunnel-abc123.trycloudflare.com` refreshed in place.)

**Edge cases handled:** logs with no URL, dead/rotating URL mid-transition (refused until it actually 200s), multiple historical URLs in logs, markdown rows with URL in both text and href, prose mentions, missing README, missing `.coding-hermes/` dir (auto-created), and double-run idempotency. **Not verifiable in this sandbox:** a real trycloudflare endpoint (no network access to one) and the actual git repo — the script's write targets and grep pipeline were validated on exact-match fixtures, and the script refuses (exit 1/2) rather than guessing when the live tunnel can't be confirmed, which is the safe failure mode for production.

---
{"model": "deepseek-v4-flash", "problem_class": "docs-tunnel-url-rotation", "result": "passed", "tests": 9}
Generated from the verified corpus · MIT licensedBack to the catalog