From 49b180e79747e23ab0d0edffa1054461ce89f6a6 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Mon, 10 Aug 2026 01:04:58 +0000 Subject: [PATCH 1/2] Add /api/version and bin/wait-deploy so deploys are observable from any machine --- README.md | 23 ++++++++++++++++++++--- bin/wait-deploy | 43 +++++++++++++++++++++++++++++++++++++++++++ main.py | 10 ++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100755 bin/wait-deploy diff --git a/README.md b/README.md index 09c7e1c..83db734 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,34 @@ uvicorn main:app --reload | `static/` | `index.html`, `app.js`, `style.css` — Vue 3 loaded from unpkg | | `requirements.txt` | Python deps | | `Procfile` | Start command; **nixpacks needs this** or the deploy has nothing to run | +| `bin/wait-deploy` | Blocks until the live site serves your latest commit | | `Dockerfile.dev`, `docker-compose.yml` | Local dev only — production does not use them | ## Deployment -Push to `main` → Forgejo webhook → Coolify rebuilds with nixpacks → live in -roughly 25 seconds. +Push to `main` → Forgejo webhook → Coolify rebuilds with nixpacks → live. +A nixpacks rebuild takes **90 seconds to 2 minutes** — longer than the static +sites, which go in ~25s. Nothing changes on the site until the new container +swaps in at the very end. + +To know when your commit is actually live, rather than guessing: ```bash -git push +git push && bin/wait-deploy ``` +It polls `/api/version` (which returns the `SOURCE_COMMIT` Coolify bakes into +the container) until it matches your local `HEAD`, then exits. Runs from any +machine — no Coolify token, no SSH tunnel. Check by hand any time with: + +```bash +curl -s https://chart.amow.com/api/version +``` + +One more trap worth naming: if your change only touches an `/api` endpoint, the +HTML is byte-identical and a browser refresh looks like nothing happened even +after a successful deploy. Verify the endpoint, not the page. + Two things to know before changing the deploy config: - The Coolify domain is registered as `https://chart.amow.com:8000`. The port diff --git a/bin/wait-deploy b/bin/wait-deploy new file mode 100755 index 0000000..3439c55 --- /dev/null +++ b/bin/wait-deploy @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Block until the deployed site is serving the commit at local HEAD. +# +# git push && bin/wait-deploy +# +# Works from any machine — it only reads the public /api/version endpoint, so +# no Coolify token and no SSH tunnel are needed. +set -uo pipefail + +URL=${URL:-https://chart.amow.com} +TIMEOUT=${TIMEOUT:-300} + +want=$(git rev-parse HEAD) || exit 1 +short=${want:0:8} + +echo "waiting for $URL to serve $short (timeout ${TIMEOUT}s)" + +start=$(date +%s) +last="" +while :; do + now=$(date +%s) + elapsed=$(( now - start )) + if [ "$elapsed" -ge "$TIMEOUT" ]; then + echo "TIMEOUT after ${elapsed}s — still serving ${last:-unknown}" + echo "check the build log: https://chart.amow.com is fine but Coolify may have failed" + exit 1 + fi + + # 502 during the rolling container swap is normal; keep polling. + got=$(curl -fsS -m 5 "$URL/api/version" 2>/dev/null \ + | sed -n 's/.*"commit"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + + if [ "$got" = "$want" ]; then + echo "deployed ${short} after ${elapsed}s" + exit 0 + fi + + if [ "$got" != "$last" ]; then + echo " ${elapsed}s: serving ${got:-unreachable}" + last="$got" + fi + sleep 3 +done diff --git a/main.py b/main.py index a304d7c..8eec074 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ Placeholder app: serves the Vue 3 single-page frontend from static/ and a couple of JSON endpoints under /api. Replace the endpoints as the real app takes shape; the serving/deploy wiring below does not need to change. """ +import os from pathlib import Path from fastapi import FastAPI @@ -13,6 +14,9 @@ from fastapi.staticfiles import StaticFiles BASE_DIR = Path(__file__).parent STATIC_DIR = BASE_DIR / "static" +# Coolify injects the deployed commit; absent when running locally. +SOURCE_COMMIT = os.environ.get("SOURCE_COMMIT", "dev") + app = FastAPI(title="chart") app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") @@ -23,6 +27,12 @@ def health(): return {"status": "ok", "service": "chart"} +@app.get("/api/version") +def version(): + """Which commit is actually serving. `bin/wait-deploy` polls this.""" + return {"commit": SOURCE_COMMIT} + + @app.get("/api/hello") def hello(): return {"msg": "this is fing awesome"} From e3459aa72e90beae724958659f9676c22149db8c Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Mon, 10 Aug 2026 01:05:51 +0000 Subject: [PATCH 2/2] Correct rebuild timings in README with measured warm/cold numbers --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 83db734..51de6cc 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ uvicorn main:app --reload ## Deployment Push to `main` → Forgejo webhook → Coolify rebuilds with nixpacks → live. -A nixpacks rebuild takes **90 seconds to 2 minutes** — longer than the static -sites, which go in ~25s. Nothing changes on the site until the new container -swaps in at the very end. +Rebuild time depends on whether Docker's build cache is warm: measured at +**~20s warm** (you pushed recently) and **~90s cold** (the cache goes stale +after an idle hour or so, which is the usual case). Nothing changes on the site +until the new container swaps in at the very end, so the old version keeps +serving for the whole build. To know when your commit is actually live, rather than guessing: